npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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.

npm version bundle size license TypeScript

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-zeugma

Quick 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.ReactNode or 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. |