react-splitkit
API Reference

LayoutProvider

The root context provider that owns the layout store and tab registry.

LayoutProvider is the top-level wrapper for every layout. It creates an isolated Zustand store and makes the tab registry available to all descendants. Multiple providers can coexist on the same page without interfering with each other.

import { LayoutProvider } from 'react-splitkit';
 
<LayoutProvider initialLayout={layout} registry={registry}>
  <LayoutRoot renderPanel={renderPanel} />
</LayoutProvider>

Props

initialLayout (required)

initialLayout: LayoutNode

The initial shape of the layout tree. Build it with createPanel and createSplit:

import { createPanel, createSplit } from 'react-splitkit';
 
const layout = createSplit('root', 'horizontal', [
  createPanel('left', [{ id: 't1', tabType: 'editor', title: 'Editor' }]),
  createPanel('right', [{ id: 't2', tabType: 'preview', title: 'Preview' }]),
]);

initialLayout is read once on mount — the same way React's defaultValue works on an uncontrolled input. After mount the store owns the layout. Changing this prop on a subsequent render does nothing.

If your sizes don't sum to 100, the library normalises them automatically so they do. You can also call normalize(layout) yourself before passing it in — useful when constructing layouts programmatically:

import { normalize } from 'react-splitkit';
 
const layout = normalize(createSplit('root', 'horizontal', [
  createPanel('a', [...]),
  createPanel('b', [...]),
]));

To swap the layout at runtime, use REPLACE_LAYOUT:

import { useLayout } from 'react-splitkit';
 
const { dispatch } = useLayout();
 
// Switch to a different saved workspace, load a user's layout, etc.
dispatch({ type: 'REPLACE_LAYOUT', layout: newLayout });

dispatch comes from useLayout. See all available actions there.

To fully reset the store (different user, different mode), remount with a new key:

<LayoutProvider
  key={userId}           // new key = new store = fresh initialLayout
  initialLayout={userLayout}
  registry={registry}
/>

registry (required)

registry: TabRegistry

Maps tabType strings to render functions and metadata. See TabRegistry for the full shape.

const registry: TabRegistry = {
  editor: {
    tabType: 'editor',
    title: 'Editor',
    render: (tab) => <Editor file={tab.id} />,
    closable: true,
    minSize: 20,
  },
};

onChange

onChange?: (layout: LayoutNode, action: LayoutAction) => void

Called after every successful state mutation with the new layout tree and the action that caused it.

  • layout — the full serializable layout tree after the mutation. Save this anywhere and pass it back as initialLayout on the next mount.
  • action — the LayoutAction that triggered the change (e.g. { type: 'RESIZE_SPLIT', ... }). Use this to skip persistence for high-frequency actions like dragging:
<LayoutProvider
  initialLayout={saved ?? defaultLayout}
  registry={registry}
  onChange={(layout, action) => {
    // Skip saving mid-drag — only persist on drag end
    if (action.type === 'RESIZE_SPLIT') return;
    localStorage.setItem('my-layout', JSON.stringify(layout));
  }}
>

See the Persistence guide for database patterns, debouncing, and schema versioning.


generateId

generateId?: () => string

Custom id generator for new nodes created at runtime (splits, tabs). Defaults to a short random string. Override this for SSR determinism or cross-instance uniqueness:

import { nanoid } from 'nanoid';
 
<LayoutProvider generateId={() => nanoid()} ... />

Multiple providers

Each LayoutProvider instance is fully isolated — its own store, its own registry, its own id namespace. DOM ids are auto-prefixed per provider using React's useId() so aria-controls / aria-labelledby associations never collide across instances.

<div className="grid grid-cols-2 gap-4">
  <LayoutProvider initialLayout={leftLayout} registry={registry}>
    <LayoutRoot renderPanel={renderPanel} />
  </LayoutProvider>
 
  <LayoutProvider initialLayout={rightLayout} registry={registry}>
    <LayoutRoot renderPanel={renderPanel} />
  </LayoutProvider>
</div>

Replacing the layout externally

If you need to swap the entire layout tree at runtime (e.g., switching between saved workspaces), dispatch REPLACE_LAYOUT:

import { useLayout } from 'react-splitkit';
 
const { dispatch } = useLayout();
 
dispatch({ type: 'REPLACE_LAYOUT', layout: savedLayout });

Reading the registry at runtime

Two hooks let you read the registry from any descendant component:

import { useTabRegistry, useTabRegistryEntry } from 'react-splitkit';
 
// Full registry
const registry = useTabRegistry();
 
// Single entry by tabType
const editorEntry = useTabRegistryEntry('editor'); // TabRegistryEntry | undefined

Use these when a component needs to render registry metadata — for example, reading entry.icon or entry.title to display in a custom tab label, or checking entry.closable before showing a close button.

These hooks are the read-only complement to the registry prop. The registry itself is stable across renders — it doesn't cause re-renders when the layout changes.

On this page