Skip to main content

Customising the empty state

When the layout value is null — the user removed the last panel, or the tree hasn't been initialised — Mosaic renders a zeroStateView. Out of the box that's the MosaicZeroState component, a neutral placeholder with a "create new" button.

Using the default

MosaicZeroState ships with the library and is the sensible default:

Live Editor
function ZeroStateDefault() {
  return (
    <div className="live-mosaic-frame">
      <Mosaic
        renderTile={(id, path) => (
          <MosaicWindow path={path} title={`Panel ${id}`}>
            <div>{id}</div>
          </MosaicWindow>
        )}
        initialValue={null}
        zeroStateView={<MosaicZeroState />}
      />
    </div>
  );
}
Result
Loading...

Click the "Create New Window" button and the mosaic transitions out of the empty state. Under the hood that's the createNode prop being invoked and the result set as the new root.

createNode: where new panels come from

MosaicZeroState's button — and the default toolbar's "split" button — both call createNode to ask you for a new leaf key. If you don't provide one, the library has no way to invent identifiers for your domain:

import { useRef } from 'react';

function App() {
const nextId = useRef(1);
const createNode = () => `panel-${nextId.current++}`;

return (
<Mosaic<string>
renderTile={/* ... */}
createNode={createNode}
zeroStateView={<MosaicZeroState />}
initialValue={null}
/>
);
}

createNode can return a value synchronously or a promise. Returning a promise is how you implement "open a picker, let the user choose what to put in the new panel, then resolve with its key":

const createNode = async (): Promise<string> => {
const choice = await openPanelPicker();
return choice.id;
};

While the promise is pending, the split/creation action waits — no panel appears until you resolve.

A fully custom zero state

Pass any React node as zeroStateView. The obvious use case is branding the empty state to fit your app:

const zeroState = (
<div className="app-zero-state">
<img src="/logo.svg" alt="" width={64} />
<h2>No panels open</h2>
<p>Drag something from the sidebar, or start from a template.</p>
<button onClick={loadTemplate}>Load default layout</button>
</div>
);

<Mosaic
value={tree}
onChange={setTree}
zeroStateView={zeroState}
renderTile={/* ... */}
/>

Because it's just React, you can wire it up to your own state, fetch templates from the server, or embed a full onboarding flow — the library treats it as an opaque node.

When to use null vs a single leaf

If you always want something on screen, pass a single leaf as your initial value:

const INITIAL: MosaicNode<string> = 'welcome';

The user can still remove it (ending up at the zero state), but the first-load experience shows content instead of a placeholder. Use null for apps where "nothing open" is a real, valid state — IDE-style tools where tabs are transient are a good fit.