Recipes
Small patterns that come up a lot and don't need anything beyond the public API.
Maximize a panel
To show a single panel on its own, keep the full layout in state, replace
value with the panel's key, and put the saved layout back when restoring.
A bare key is a valid tree, so Mosaic renders it as one full-size window.
function MaximizeExample() { const [tree, setTree] = React.useState({ type: 'split', direction: 'row', children: ['a', { type: 'split', direction: 'column', children: ['b', 'c'] }], }); const [savedTree, setSavedTree] = React.useState(null); const isMaximized = savedTree !== null; const toggleMaximize = (id) => { if (isMaximized) { setTree(savedTree); setSavedTree(null); } else { setSavedTree(tree); setTree(id); } }; return ( <div className="live-mosaic-frame"> <Mosaic value={tree} onChange={setTree} renderTile={(id, path) => ( <MosaicWindow path={path} title={`Panel ${id}`} toolbarControls={ <DefaultToolbarButton title={isMaximized ? 'Restore' : 'Maximize'} text={isMaximized ? 'Restore' : 'Maximize'} className="" onClick={() => toggleMaximize(id)} /> } > <div style={{ padding: 20 }}>Panel {id}</div> </MosaicWindow> )} /> </div> ); }
Because you own the maximized state, you also decide what else is allowed in it. The example only shows a Restore button, so the user can't remove or split the panel while the rest of the layout is stashed away.
If you only want the panel to take most of the space and keep the others
visible, mosaicActions.expand(path, percentage) from MosaicContext does
that without swapping the tree.
Drag the whole tile, without a toolbar
MosaicWindowContext exposes connectDragSource, the same connector the title
bar uses. Wrap any element inside the window with it and that element becomes a
drag handle. Here it's the whole body, and the toolbar is hidden with CSS:
function WholeTileDragExample() { function DragAnywhere({ children }) { const { mosaicWindowActions } = React.useContext(MosaicWindowContext); return mosaicWindowActions.connectDragSource( <div style={{ height: '100%', padding: 20, cursor: 'move' }}>{children}</div>, ); } return ( <div className="live-mosaic-frame no-toolbar"> <style>{'.no-toolbar .mosaic-window-toolbar { display: none; }'}</style> <Mosaic renderTile={(id, path) => ( <MosaicWindow path={path} title={id} draggable={false} renderToolbar={() => <div />}> <DragAnywhere>Drag me anywhere: {id}</DragAnywhere> </MosaicWindow> )} initialValue={{ type: 'split', direction: 'row', children: ['a', { type: 'split', direction: 'column', children: ['b', 'c'] }], }} /> </div> ); }
draggable={false} stops the (now hidden) toolbar from being a drag handle.
renderToolbar replaces the default title and buttons with an empty element.
If your tiles have inputs or selectable text, wrap a smaller area (a grip
icon, a header) instead of the whole body so those keep working.
Swap tiles instead of splitting
By default, dropping a window on another one splits it along the edge you drop
on. Set dropBehavior to change that:
'swap': the two windows trade places. Every pane keeps its size, and the only drop target is the whole window.'split-and-swap': the edges still split, and the centre of a window swaps.
function SwapExample() { const [dropBehavior, setDropBehavior] = React.useState('swap'); // Keep one layout object, so switching modes doesn't reset your swaps const [initialLayout] = React.useState({ type: 'split', direction: 'row', splitPercentages: [30, 70], children: [ 'a', { type: 'split', direction: 'column', splitPercentages: [60, 40], children: ['b', 'c'], }, ], }); return ( <> <select value={dropBehavior} onChange={(event) => setDropBehavior(event.target.value)} style={{ marginBottom: 8 }} > <option value="swap">swap</option> <option value="split-and-swap">split-and-swap</option> <option value="split">split (default)</option> </select> <div className="live-mosaic-frame"> <Mosaic dropBehavior={dropBehavior} renderTile={(id, path) => ( <MosaicWindow path={path} title={`Panel ${id}`}> <div style={{ padding: 20 }}>Drag my title onto another panel</div> </MosaicWindow> )} initialValue={initialLayout} /> </div> </> ); }
To swap nodes from your own code, createSwapUpdates(tree, pathA, pathB)
returns the updates for updateTree.
Know which panel was removed
onChange gets a second meta argument that says what changed. Panels leave
the layout in three ways: a removal carries the removed node (a panel key, or a
whole subtree), closing a tab carries the tab, and a replace carries what was
there before:
onChange={(next, meta) => {
if (meta?.type === 'remove') onPanelsRemoved(getLeaves(meta.node));
if (meta?.type === 'tab-remove') onPanelsRemoved([meta.tab]);
if (meta?.type === 'replace' && meta.previous != null) {
onPanelsRemoved(getLeaves(meta.previous));
}
setTree(next);
}}
On versions before meta was added, compare leaves instead:
getLeaves(tree).filter((id) => !getLeaves(next).includes(id)).
A layout bigger than the screen
Mosaic fills its parent. To get a scrollable canvas, give it a larger parent
inside a scroll container:
<div style={{ height: '100vh', overflow: 'auto' }}>
<div style={{ width: 2000, height: 1500 }}>
<Mosaic renderTile={/* ... */} initialValue={/* ... */} />
</div>
</div>