Custom Section
A Section is the atomic unit of <SidePanel />. It describes what icon/label the side tab shows and what content appears when that tab is active.
This page covers 3 scenarios: add / replace / on-demand.
Section type
export type Section = {
/** Unique identifier */
name: string;
/** Tab button rendered in the side column */
Tab: React.ComponentType<{
onClick: () => void;
active: boolean;
}>;
/** Content rendered when the tab is active */
Panel: React.ComponentType<{
store: StoreType;
}>;
/**
* Whether to appear in the persistent tab list.
* false = hide tab; panel can still be opened via store.openSidePanel(name).
* Defaults to true.
*/
visibleInList?: boolean;
};
Scenario 1: Add a new section
A minimal example — an "AI Assistant" panel:
import { observer } from 'mobx-react-lite';
import { Sparkles } from 'lucide-react';
import {
SidePanel,
SectionTab,
DEFAULT_SECTIONS,
} from '@ydesign/react-editor/side-panel';
import type { Section } from '@ydesign/react-editor/side-panel';
const AISection: Section = {
name: 'ai',
Tab: observer(props => (
<SectionTab name="AI Assistant" {...props}>
<Sparkles size={20} />
</SectionTab>
)),
Panel: observer(({ store }) => {
return (
<div style={{ padding: 12 }}>
<h3>Generate image with AI</h3>
<button
onClick={async () => {
const url = await callYourAIService('a cute kitten');
store.addElement({
type: 'image',
src: url,
left: 100,
top: 100,
width: 500,
height: 500,
});
}}
>
Generate one
</button>
<p style={{ marginTop: 16 }}>
Canvas: {store.width} × {store.height}, {store.objects.length} elements
</p>
</div>
);
}),
};
const sections = [...DEFAULT_SECTIONS, AISection];
<SidePanel store={store} sections={sections} defaultSection="ai" />;
Key points:
- Wrap your
TabwithSectionTabto match hover / active / dark-light theming of the built-in tabs - Wrap your
Panelwithobserver(otherwise it won't re-render on store changes) storeis the MST instance — callstore.addElement(...)/store.set(...)to affect the canvas
Scenario 2: Replace a built-in section
Want to swap the built-in PhotosSection for your own gallery? Just build a Section with the same name and drop it into the sections array — the later one wins.
import {
SidePanel,
SectionTab,
DEFAULT_SECTIONS,
} from '@ydesign/react-editor/side-panel';
import type { Section } from '@ydesign/react-editor/side-panel';
import { observer } from 'mobx-react-lite';
import { Image } from 'lucide-react';
import { useEffect, useState } from 'react';
const MyPhotosSection: Section = {
name: 'photos', // 👈 same as the built-in
Tab: observer(props => (
<SectionTab name="My Gallery" {...props}>
<Image size={20} />
</SectionTab>
)),
Panel: observer(({ store }) => {
const [list, setList] = useState<any[]>([]);
useEffect(() => {
fetch('/my-api/photos').then(r => r.json()).then(setList);
}, []);
return (
<div>
{list.map(item => (
<img
key={item.id}
src={item.thumbnail}
style={{ width: '100%', cursor: 'pointer', marginBottom: 8 }}
onClick={() =>
store.addElement({
type: 'image',
src: item.url,
left: 50,
top: 50,
width: item.width,
height: item.height,
})
}
/>
))}
</div>
);
}),
};
const sections = DEFAULT_SECTIONS.map(s =>
s.name === 'photos' ? MyPhotosSection : s
);
<SidePanel store={store} sections={sections} />;
💡 Want to keep most of the built-in panel and tweak only the grid styling? Reuse
<ImagesGrid />— it'll save you most of the layout code.
Scenario 3: Remove a section
Pass a name array and omit the ones you don't want:
<SidePanel
store={store}
sections={['templates', 'text', 'photos', 'background', 'layers']}
// upload / shapes / size / idphoto / inpaint won't appear
/>
Or filter from DEFAULT_SECTIONS:
import { DEFAULT_SECTIONS } from '@ydesign/react-editor/side-panel';
const sections = DEFAULT_SECTIONS.filter(s => s.name !== 'upload');
<SidePanel store={store} sections={sections} />;
visibleInList: on-demand panels
Some panels shouldn't live in the persistent tab list — they only make sense during a specific workflow. For example:
- AI eraser — only relevant after selecting an image and entering "eraser mode"
- ID-photo layout — only relevant after the user clicks a "Make ID photo" button
- Element-specific editor — only shown while a custom element is selected
Mark these sections with visibleInList: false:
import { autorun } from 'mobx';
const TextQuickEditSection: Section = {
name: 'text-quick-edit',
visibleInList: false, // 👈 hidden from the tab list
Panel: observer(({ store }) => {
const el = store.selectedElements[0];
if (el?.type !== 'textbox') return null;
return (
<div>
<textarea
value={el.text}
onChange={e => store.set({ text: e.target.value }, el)}
/>
</div>
);
}),
};
const sections = [...DEFAULT_SECTIONS, TextQuickEditSection];
// Open this panel based on the current selection
autorun(() => {
const el = store.selectedElements[0];
if (el?.type === 'textbox') {
store.openSidePanel('text-quick-edit');
}
});
<SidePanel store={store} sections={sections} />;
📌 This is exactly how
InpaintSection/IdphotoSectionwork inDEFAULT_SECTIONS. They'revisibleInList: false, hidden by default, and business code opens them withstore.openSidePanel('inpaint')/store.openSidePanel('idphoto')at the right moment.
Manual open / close
store.openSidePanel('ai'); // open
store.openSidePanel(''); // close
store.openedSidePanel; // current
About SectionTab
SectionTab is the shared Tab component used by every built-in Section. It accepts the Tab props (onClick / active) plus its own:
<SectionTab
name="Panel name" // text under the icon
{...props} // onClick / active from <SidePanel />
>
{/* Icon goes here — lucide-react recommended */}
<YourIcon size={20} />
</SectionTab>
Using SectionTab gives you:
- Matching hover / active colors
- Dark / light theme support
- Consistent sizing with built-in tabs
For a fully custom tab button, render your own <div onClick={props.onClick}> — just handle the active style yourself.