Skip to main content

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.

Live Editor
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>
  );
}
Result
Loading...

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:

Live Editor
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>
  );
}
Result
Loading...

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.
Live Editor
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>
    </>
  );
}
Result
Loading...

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>