react-zeugma
v6.7.2
Published
Recursive drag-and-drop dashboard layout engine for React — combining the tree-based splitting of react-mosaic with the declarative API of react-grid-layout.
Downloads
10,113
Maintainers
Readme
react-zeugma
A recursive, drag-and-drop dashboard layout engine for React. It combines the tree-based, arbitrary splitting capabilities of react-mosaic with the declarative, state-driven API model of react-grid-layout, powered by @dnd-kit/core.
It is completely style-agnostic (headless), meaning you style all container states, resizers, and drop previews with your own class names.
Installation
npm install react-zeugmaQuick Start
Initialize your layout tree with useZeugma and render the dashboard using <Zeugma>.
import { useZeugma, Zeugma, Pane, TreeNode } from 'react-zeugma'
// 1. Define the initial layout tree structure
const initialLayout: TreeNode = {
type: 'split',
direction: 'row',
splitPercentage: 30,
first: { type: 'pane', id: 'left-panel', tabs: ['left-panel'], activeTabId: 'left-panel' },
second: { type: 'pane', id: 'right-panel', tabs: ['right-panel'], activeTabId: 'right-panel' },
}
// 2. Build your custom pane wrapper
function DashboardPane({ id }: { id: string }) {
return (
<Pane id={id}>
<div className="flex flex-col h-full bg-zinc-900 border border-zinc-700">
<Pane.DragHandle className="p-2 bg-zinc-800 cursor-grab text-zinc-300 font-semibold">
{id}
</Pane.DragHandle>
<Pane.Content className="flex-1 p-4 text-zinc-400">
{(tab) => <div>Active Tab Content: {tab.id}</div>}
</Pane.Content>
</div>
</Pane>
)
}
// 3. Mount the layout controller and dashboard renderer
export default function DashboardApp() {
const controller = useZeugma({ initialLayout })
return (
<div className="w-screen h-screen">
<Zeugma controller={controller} renderPane={(paneId) => <DashboardPane id={paneId} />} />
</div>
)
}API Reference
Components
<Zeugma>
The root provider and layout renderer. It configures the drag-and-drop context, calculates panel positions, and renders resize splitters.
Usage
import { Zeugma } from 'react-zeugma'
;<Zeugma
controller={controller}
renderPane={(paneId) => <MyPane id={paneId} />}
resizerSize={4}
dragActivationDistance={8}
snapThreshold={8}
minSplitPercentage={5}
maxSplitPercentage={95}
enableDragToDismiss={false}
dismissThreshold={60}
classNames={{
dashboard: 'bg-zinc-950',
pane: 'rounded-lg overflow-hidden',
resizer: 'bg-zinc-800 hover:bg-indigo-500 transition-colors',
dropPreview: 'bg-indigo-500/20 border border-indigo-500',
}}
onRemove={(paneId) => console.log(`Pane ${paneId} closed`)}
onResizeEnd={(currentNode, percentage) => console.log('Resized to', percentage)}
/>Props
| Property | Description | Type | Default |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
| controller | The layout state controller returned by useZeugma(options). | ZeugmaController | - |
| children | Children components rendered inside the context provider. | ReactNode | - |
| renderPane | Callback function to map active pane IDs to custom pane structures. Required in standalone mode (without children) and must not be passed in provider mode. | (paneId: string) => ReactNode | - |
| renderDragOverlay | Custom overlay renderer function for the drag-under-cursor preview. | (active: DragOverlayActiveItem) => ReactNode | - |
| classNames | CSS class name mapping overrides for custom dashboard and overlay styling. | ZeugmaClassNames | - |
| resizerSize | Thickness of the split resizer bars in pixels. | number | 4 |
| dragActivationDistance | Minimum pointer drag distance (in pixels) required to activate dragging. | number | 8 |
| snapThreshold | Threshold in pixels to snap layout resizers to adjacent edges. | number | 8 |
| minSplitPercentage | Minimum split limit percentage allowed for resized panes. | number | 5 |
| maxSplitPercentage | Maximum split limit percentage allowed for resized panes. | number | 95 |
| enableDragToDismiss | Enables drag-out-to-dismiss gesture for widgets. | boolean | false |
| dismissThreshold | Distance in pixels outside container bounds required to trigger dismissal. | number | 60 |
| onRemove | Callback triggered when a pane is removed. | (paneId: string) => void | - |
| onDragStart | Callback triggered when a drag gesture begins. | (activeId: string) => void | - |
| onDragEnd | Callback triggered when a drag gesture ends, containing active pane, target pane, and action metadata. | (activeId: string, overId: string \| null, dropAction: { type: 'split' \| 'move'; direction?: SplitDirection; position?: 'top' \| 'bottom' \| 'left' \| 'right' \| 'center' } \| null) => void | - |
| onResizeStart | Callback triggered when resizing begins. | (currentNode: SplitNode) => void | - |
| onResize | Callback triggered during pane resizing. | (currentNode: SplitNode, percentage: number) => void | - |
| onResizeEnd | Callback triggered when pane resizing completes. | (currentNode: SplitNode, percentage: number) => void | - |
| onDismissIntentChange | Callback triggered when drag-out dismiss intent changes. | (paneId: string \| null) => void | - |
| persist | Layout persistence configuration in localStorage. If true, uses default options. | boolean \| ZeugmaPersistOptions | false |
ZeugmaPersistOptions
interface ZeugmaPersistOptions {
/** Whether layout persistence is enabled. Defaults to true if this configuration object is provided. */
enabled?: boolean
/** The key used for localStorage persistence. Defaults to 'zeugma-layout'. */
key?: string
}<PaneTree>
Recursively renders the dashboard grid hierarchy (resizers, split panels, and active pane contents). Must be rendered when using <Zeugma> as a context provider.
Usage
import { Zeugma, PaneTree } from 'react-zeugma'
;<Zeugma controller={controller}>
<div className="workspace">
<PaneTree renderPane={(paneId) => <MyPane id={paneId} />} />
</div>
</Zeugma>Props
| Property | Description | Type | Default |
| --------------- | ----------------------------------------------------------------------------------------- | ------------------------------- | ------- |
| renderPane | Required. Callback function mapping unique pane IDs to custom <Pane> components. | (paneId: string) => ReactNode | - |
| tree | Optional layout subtree to render (defaults to the root layout tree from the controller). | TreeNode \| null | - |
| resizerSize | Optional override for the thickness of split resizer handles in pixels. | number | 4 |
| snapThreshold | Optional override for the snapping threshold of resizer handles in pixels. | number | 8 |
<Pane>
Wraps each individual pane/widget within the dashboard, establishing drag-and-drop boundaries.
<Pane.DragHandle>: Defines the interactive header or area used to drag the pane.<Pane.Content>: Renders the active tab's content. Accepts a child render function(tab) => React.ReactNodeor static ReactNode.<Pane.Controls>: Renders standard control buttons for closing or maximizing the pane.
Usage
import { Pane } from 'react-zeugma'
;<Pane id="pane-1" locked={false}>
<Pane.DragHandle className="p-2 cursor-grab bg-zinc-800">
<span>Pane Title</span>
</Pane.DragHandle>
<Pane.Controls />
<Pane.Content className="p-4">
{(tab) => <div>Rendered content for tab: {tab.id}</div>}
</Pane.Content>
</Pane>Props
| Property | Description | Type | Default |
| ---------- | ------------------------------------------------------------------ | --------------------- | ------- |
| id | The unique ID corresponding to the layout node. | string | - |
| children | Children components rendered inside the pane. | React.ReactNode | - |
| style | Optional inline CSS styles applied to the outer pane container. | React.CSSProperties | - |
| locked | Optional override to lock this specific pane and disable dragging. | boolean | false |
<Tabs>
A helper component to render and reorder a list of tabs inside a pane.
Usage
import { Tabs } from 'react-zeugma'
;<Tabs
tabs={['tab1', 'tab2']}
activeTabId="tab1"
selectTab={(tabId) => console.log('Select tab:', tabId)}
removeTab={(tabId) => console.log('Close tab:', tabId)}
renderTab={({ tabId, activeTabId, onSelect, onRemove }) => (
<button
onClick={onSelect}
className={`px-3 py-1 ${tabId === activeTabId ? 'bg-zinc-800 text-white' : 'text-zinc-400'}`}
>
{tabId}
<span
className="ml-2 cursor-pointer"
onClick={(e) => {
e.stopPropagation()
onRemove()
}}
>
×
</span>
</button>
)}
/>Props
| Property | Description | Type | Default |
| -------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
| tabs | Array of tab IDs. | string[] | - |
| activeTabId | The currently active tab ID. | string | - |
| locked | Whether tab dragging/reordering is disabled. | boolean | false |
| tabsMetadata | Metadata mapping associated with each tab in the pane. | Record<string, Record<string, unknown>> | - |
| selectTab | Callback when a tab is selected. | (id: string) => void | - |
| removeTab | Callback when a tab is closed. | (id: string) => void | - |
| classNames | Custom class names for the container and tabs. | { container?: string; tab?: string \| ((tabId: string) => string) } | - |
| styles | Custom CSS style overrides for the container and tabs. | { container?: CSSProperties; tab?: CSSProperties \| ((tabId: string) => CSSProperties) } | - |
| renderTab | Render prop function called for each tab item. | (props: { tabId: string; activeTabId: string; isDragging: boolean; isOver: boolean; metadata?: Record<string, unknown>; onSelect: () => void; onRemove: () => void }) => ReactNode | - |
Hooks
useZeugma(options)
A custom state hook that instantiates the dashboard layout engine and returns the controller.
Usage
import { useZeugma } from 'react-zeugma'
const controller = useZeugma({
initialLayout: myInitialLayoutTree, // used on mount
layout: myControlledLayout, // used for controlled mode
onChange: (nextLayout) => {}, // layout update callback
locked: false, // lock all dragging and resizing
fullscreenPaneId: null, // ID of pane to zoom fullscreen
onFullscreenChange: (paneId) => {}, // callback when pane zoom toggled
})Options
| Parameter | Description | Type | Default |
| -------------------- | --------------------------------------------------------------------------- | --------------------------------------- | ------- |
| initialLayout | Initial layout tree structure. Only used on mount. | TreeNode \| null | null |
| layout | Controlled layout tree structure. Hook runs in controlled mode if provided. | TreeNode \| null | null |
| onChange | Callback triggered when the layout tree updates. | (newLayout: TreeNode \| null) => void | - |
| fullscreenPaneId | Controlled fullscreen pane ID. | string \| null | null |
| onFullscreenChange | Callback triggered when fullscreen state toggles. | (paneId: string \| null) => void | - |
| locked | Global lock status to disable resizing and drag-and-drop operations. | boolean | false |
useZeugmaContext()
Context hook to retrieve layout state, queries, and mutation actions from anywhere under the <Zeugma> tree.
Usage
import { useZeugmaContext } from 'react-zeugma'
const { layout, locked, setLocked, addTab, removePane, selectTab, findPaneById } =
useZeugmaContext()Context Values
| Property / Method | Description | Type |
| ----------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| layout | The current active layout tree structure. | TreeNode \| null |
| fullscreenPaneId | The ID of the maximized fullscreen pane. | string \| null |
| locked | Whether the dashboard layout is globally locked. | boolean |
| setLayout | React state setter to update the layout tree. | Dispatch<SetStateAction<TreeNode \| null>> |
| setFullscreenPaneId | Updates the active fullscreen pane ID. | (paneId: string \| null) => void |
| setLocked | Updates the global layout lock state. | Dispatch<SetStateAction<boolean>> |
| removePane | Removes a pane and collapses the split. | (paneId: string) => void |
| addTab | Adds a tab to a pane, or splits/creates one if target is omitted. | (tabId: string, targetPaneId?: string, metadata?: Record<string, unknown>) => void |
| updateMetadata | Mutates a specific tab's metadata. | (id: string, updater: (current: Record<string, unknown> \| undefined) => Record<string, unknown> \| undefined) => void |
| updatePaneLock | Toggles the lock status of a specific pane. | (paneId: string, locked: boolean) => void |
| selectTab | Focuses/activates a tab within a pane. | (paneId: string, tabId: string) => void |
| mergeTab | Programmatically drags and drops a tab from one pane to another. | (draggedTabId: string, targetPaneId: string) => void |
| removeTab | Programmatically closes a tab. | (tabId: string) => void |
| splitPane | Programmatically splits a pane node and adds a new one. | (targetId: string, direction: SplitDirection, splitType: 'left' \| 'right' \| 'top' \| 'bottom', paneToAdd: string) => void |
| updateSplitPercentage | Updates a SplitNode percentage. | (currentNode: SplitNode, percentage: number) => void |
| moveTab | Reorders a tab next to another. | (draggedTabId: string, targetTabId: string, position?: 'before' \| 'after') => void |
| findPaneById | Queries a PaneNode by its unique ID. | (paneId: string) => PaneNode \| null |
| findPaneContainingTab | Queries the parent PaneNode of a tab ID. | (tabId: string) => PaneNode \| null |
| findTabById | Queries detailed tab location and state metadata. | (tabId: string) => TabDetails \| null |
| getTabMetadata | Gets metadata for a tab ID. | (tabId: string) => Record<string, unknown> \| undefined |
| getActiveTabMetadata | Gets metadata for the active tab in a pane. | (paneId: string) => Record<string, unknown> \| undefined |
usePaneContext()
Context hook to access the state and actions of a specific pane. Must be used inside a <Pane> component.
Usage
import { usePaneContext } from 'react-zeugma'
const {
id,
tabs,
activeTabId,
isDragging,
isFullscreen,
toggleFullscreen,
remove,
selectTab,
removeTab,
updateMetadata,
} = usePaneContext()Context Values
| Property / Method | Description | Type |
| ------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| id | The ID of the current pane. | string |
| tabs | List of tab IDs inside the pane. | string[] |
| activeTabId | Currently active tab ID. | string |
| isDragging | true if this pane is being dragged. | boolean |
| isFullscreen | true if this pane is maximized. | boolean |
| toggleFullscreen | Toggles maximized state for this pane. | () => void |
| remove | Removes this pane from the layout tree. | () => void |
| selectTab | Activates a tab within this pane. | (tabId: string) => void |
| removeTab | Closes a tab from this pane. | (tabId: string) => void |
| metadata | Active tab's custom metadata. | Record<string, unknown> \| undefined |
| updateMetadata | Updates active tab's metadata. | (updater: (current: Record<string, unknown> \| undefined) => Record<string, unknown> \| undefined) => void |
| locked | Whether the pane or the dashboard is locked. | boolean |
| tabsMetadata | Tab metadata mapping for all tabs inside this pane. | Record<string, Record<string, unknown>> \| undefined |
| updateTabMetadata | Updates metadata for a specific tab in the pane. | (tabId: string, updater: (current: Record<string, unknown> \| undefined) => Record<string, unknown> \| undefined) => void |
useResizer(props)
Low-level hook for implementing custom pane resizing handles.
Usage
import { useResizer } from 'react-zeugma'
const handlePointerDown = useResizer({
containerRef,
isRow,
direction,
splitPercentage,
resizerSize,
snapThreshold,
layout,
currentNode,
onLayoutChange: (nextTree) => {},
})Layout Utilities
Import utility functions from react-zeugma/utils to programmatically query or update the serialized layout tree.
Usage
import {
generateUniqueId,
splitPane,
removePane,
addTab,
removeTab,
selectTab,
mergeTab,
moveTab,
findPaneById,
findPaneContainingTab,
findTabById,
computeLayout,
} from 'react-zeugma/utils'
// 1. Generate a random unique pane ID
const newPaneId = generateUniqueId()
// 2. Programmatically split a target pane in the tree
const updatedTree = splitPane(currentTree, 'explorer', 'row', 'right', 'terminal')
// 3. Programmatically add a tab to a pane
const updatedTree = addTab(currentTree, 'editor', 'new-file.js', { status: 'unsaved' })
// 4. Find which pane contains a specific tab
const parentPane = findPaneContainingTab(currentTree, 'new-file.js')Styling & Class Names
react-zeugma is 100% headless. You must style resizers, previews, and containers by providing custom class names.
Usage
<Zeugma
controller={controller}
classNames={{
dashboard: 'dashboard-root',
pane: 'pane-wrapper',
resizer: 'custom-resizer-line',
dropPreview: 'drop-preview-box',
tabDropPreview: 'tab-line-preview',
}}
/>Class Names Mapping
| Class Key | Description |
| ------------------------ | ------------------------------------------------------------------- |
| dashboard | Root dashboard grid container. |
| dashboardDismissActive | Dashboard container when active item is dragged outside to dismiss. |
| dashboardLocked | Dashboard container when layout is globally locked. |
| pane | Outer container div of each <Pane>. |
| paneLocked | Pane container wrapper when locked. |
| paneContainer | Pane inner content container wrapper. |
| paneHeader | Drag header wrapper inside the pane. |
| paneControls | Controls wrapper container (maximizing, close, lock). |
| paneButton | Maximize/Close control buttons. |
| dropPreview | Preview indicator box for edge layout splits. |
| rootDropPreview | Preview indicator for full layout splits. |
| dragOverlay | Absolute portal wrapper following the dragging cursor. |
| paneDragPreview | Outer wrapper container of a pane drag preview node. |
| tabDragPreview | Outer wrapper container of a tab drag preview node. |
| resizer | Pointer-drag splitter handle bars. |
| dismissPreview | Background indicator showing visual drag-to-dismiss zones. |
| lockedPreview | Hover visual feedback indicator for locked pane zones. |
| tabDropPreview | Tab list insertion indicator line. |
| tabSeparator | Line separator between static tabs. |
| tabContentWrapper | Custom tab content element wrapper. |
| tabsContainer | Layout tabs container header bar. |
| tab | Individual tab list items. |
| tabCloseButton | Close button inside a tab item. |
| dragHandle | Drag target region. |
