Skip to main content

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:

  1. Wrap your Tab with SectionTab to match hover / active / dark-light theming of the built-in tabs
  2. Wrap your Panel with observer (otherwise it won't re-render on store changes)
  3. store is the MST instance — call store.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 / IdphotoSection work in DEFAULT_SECTIONS. They're visibleInList: false, hidden by default, and business code opens them with store.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.


Next