--- id: custom-toolbar title: Custom toolbars sidebar_position: 1 --- # Custom toolbars Every `MosaicWindow` renders a title bar with a default set of controls on the right. You can replace that set entirely, add to it, or style it — all by passing React nodes to the `toolbarControls` prop. ## The default toolbar Out of the box you get split, expand and remove buttons. The presets are exported so you can reuse them: ```tsx import { DEFAULT_CONTROLS_WITH_CREATION, DEFAULT_CONTROLS_WITHOUT_CREATION, } from 'react-mosaic-component'; ``` - `DEFAULT_CONTROLS_WITH_CREATION` — Split, Expand, Remove - `DEFAULT_CONTROLS_WITHOUT_CREATION` — Expand, Remove (no Split) Passing `toolbarControls={DEFAULT_CONTROLS_WITHOUT_CREATION}` disables the split button without forcing you to rebuild the toolbar. ## Editable example — change a button color live Edit the `buttonColor` constant below and watch the toolbar update. This is the entire value of live-coding docs: the example _is_ the API surface. ```tsx live function CustomToolbarExample() { const buttonColor = '#106ba3'; // try '#db3737', '#0f9960', '#d9822b' const toolbar = ( alert('custom action!')} > Custom ); return ( ( Edit `buttonColor` above to re-theme the toolbar. )} initialValue={{ type: 'split', direction: 'row', children: ['left', 'right'], }} /> ); } ``` ## Building your own buttons Each default button is a thin wrapper around `DefaultToolbarButton`, which handles the icon+label+click plumbing. You can compose your own: ```tsx live function CustomButtonExample() { function StarButton() { return ( alert('starred!')} > ★ ); } const toolbar = ( ); return ( ( Panel {id} )} initialValue={{ type: 'split', direction: 'row', children: ['a', 'b'] }} /> ); } ``` ## Accessing window actions from a custom button Custom buttons often need to operate on the panel they live in — remove it, expand it, replace its content. `MosaicWindowContext` exposes those actions: ```tsx import { useContext } from 'react'; import { MosaicWindowContext, DefaultToolbarButton, } from 'react-mosaic-component'; function DuplicateButton() { const { mosaicWindowActions } = useContext(MosaicWindowContext); return ( mosaicWindowActions.split()} > ⎘ ); } ``` Similarly, `MosaicContext` gives you tree-level actions (`hide`, `expand`, `remove`, `replaceWith`, `updateTree`) for operations that aren't scoped to a single window. ## Toolbars inside tab groups Tab groups render their own toolbar: tab buttons on the left, then on the right a library-owned drag handle followed by a controls cluster (add tab, split, remove by default). You can reshape the controls cluster without giving up drag-and-drop — that stays with the library. The customization props, ordered from least to most invasive: | Prop | What it swaps | Library keeps owning | |---|---|---| | `renderTabTitle` | Content inside each tab button | Drag, close, DnD | | `renderTabToolbarControls` | The right-side controls cluster (add, split, remove, …) | Drag handle, drop targets | | `renderTabToolbar` | The entire tab bar (escape hatch) | Nothing — you re-wire DnD yourself | Prefer the first two. `renderTabToolbar` is a last resort; opting into it means re-implementing tab rendering, drop targets, and drag handles. ### Composing your own controls cluster `renderTabToolbarControls` receives `{ tabs, activeTabIndex, path, mosaicId }` and returns a `ReactNode`. You decide which buttons are present, in what order, and when. The library injects its drag handle as a sibling before your controls, so you never touch `react-dnd`. The tab-specific buttons are exported so you can drop them in directly: ```tsx import { DefaultAddTabButton, TabSplitButton, TabRemoveButton, TabExpandButton, } from 'react-mosaic-component'; ``` ### Per-tab controls Show buttons that depend on which tab is active — e.g. a preview action that's only meaningful for Markdown files. ```tsx live function PerTabControlsExample() { const isMarkdown = (id) => typeof id === 'string' && id.endsWith('.md'); return ( ( Open: **{id}** {isMarkdown(id) && (Preview available)} )} renderTabToolbarControls={({ tabs, activeTabIndex, path }) => ( <> {isMarkdown(tabs[activeTabIndex]) && ( alert('preview ' + tabs[activeTabIndex])} > 👁 )} )} initialValue={{ type: 'tabs', tabs: ['readme.md', 'index.ts', 'notes.md'], activeTabIndex: 0, }} /> ); } ``` Switch tabs: the preview button appears only for `.md` files. The drag handle between the controls and the tab row is still there — you never had to think about it. ### Capping the number of tabs Omit `DefaultAddTabButton` when the tab group is full. Because you compose the cluster yourself, conditional rendering is just a React expression. ```tsx live function TabLimitExample() { const MAX_TABS = 4; let counter = 0; return ( `tab-${++counter}`} renderTile={(id, path) => ( Panel {id} )} renderTabToolbarControls={({ tabs, path }) => ( <> {tabs.length < MAX_TABS && } )} initialValue={{ type: 'tabs', tabs: ['a', 'b'], activeTabIndex: 0, }} /> ); } ``` Add tabs until you reach four — the `+` disappears. ### Fully custom add button Swap `DefaultAddTabButton` for your own element. Call `mosaicActions.addTab(path)` to run the library's tab-add logic: it appends to an existing tab group, or converts a leaf into a 2-tab group when `path` points at a leaf. Returns a promise that rejects if `createNode` isn't set. ```tsx live function CustomAddButtonExample() { let counter = 0; function CustomAddButton({ path }) { const { mosaicActions } = React.useContext(MosaicContext); return ( { if (window.confirm('Open a new tab?')) { mosaicActions.addTab(path); } }} > ➕ New ); } return ( `tab-${++counter}`} renderTile={(id, path) => ( Panel {id} )} renderTabToolbarControls={({ path }) => ( <> )} initialValue={{ type: 'tabs', tabs: ['a', 'b'], activeTabIndex: 0, }} /> ); } ``` The same `mosaicActions.addTab(path)` is available anywhere in your app — from keyboard shortcut handlers, menu items, command palettes — not just from inside the renderer. ### Escape hatch: `renderTabToolbar` If none of the slots above fit, `renderTabToolbar` hands you the entire tab bar. You receive `{ tabs, activeTabIndex, path, DraggableTab }` and must return the full toolbar element, including tab rendering, drop targets, and anything else. This is rarely what you want; reach for it only when the default layout itself is wrong for your app (e.g. vertical tabs, tabs on the bottom).