---
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).