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-dockable-desktop

v5.3.1

Published

A premium, state-of-the-art window manager and dockable layout engine for React. Supports fluid grid splits, tabbed groups, floating resizable windows, zero-unmount state preservation, context menus, and internationalization.

Readme

# React Dockable Desktop

npm version TypeScript Touch Ready license Demo Docs

Sponsor

A premium dockable layout engine for React. Build desktop-class applications with fluid split-docking grids, tabbed panels, resizable floating windows, and zero-unmount DOM preservation — by default, across docking, floating, and tab-switching alike, so WebGL contexts, live maps, and stateful editors never lose their state no matter how a panel is moved.

Live Demo  |  Full Documentation  |  API Reference


Features

  • Split-Docking Grid — drag panels to split any zone into rows/columns or group into tabbed containers
  • Workspace Edge Docking — drag to the outer edges to dock a panel as a full-width or full-height strip
  • Floating Windows — pop panels into freely resizable floating windows; 8-direction resize handles (N/NE/E/SE/S/SW/W/NW), maximize, minimize; drag to a workspace corner to anchor it there — anchored windows stack with 8 px gaps and reposition when the viewport resizes
  • Panel Overlay — per-panel overlay layer with anchored toolbars (PanelToolbar, ToolbarButton, ToolbarToggle, async search) and corner-anchored floating windows that stack, drag, and dock; usePanelFloatingWindowManager() opens N named windows dynamically from data or event handlers
  • Touch & Mobile Ready — full iPad and Android support: long-press to drag tabs, touch resize, 44px coarse-pointer targets throughout
  • Zero-Unmount DOM Persistence — panel DOM nodes are moved, never destroyed, across docking, floating, and tab-switching alike, all by default; WebGL, maps, terminals, and forms retain full state with zero integration work
  • i18n & RTL — full Right-to-Left layout support; dir="rtl" flips every control, tab order, and drop zone automatically
  • Inter-Panel Pub/Sub — lightweight typed event bus for decoupled panel-to-panel communication
  • Imperative APIWorkspaceClient opens, closes, focuses, and serializes panels from anywhere — inside or outside React
  • Layout Serialization — save and restore the full workspace as a JSON string; survives page reloads
  • 7 Built-in Skins — VSCode, macOS, Chrome, Slate, Nord, Obsidian, Tokyo — all fully themeable via CSS variables
  • Toast Notifications — imperative singleton toast.info/success/warning/error/promise() with queue, pause-on-hover, progress bar, and a ToastAdapter interface for delegating to a third-party notification library
  • Drag-Resize PrimitivesstartPointerDrag() and computeResizedRect(), the same pointer-capture mechanics and 8-directional resize math the built-in resizers use, exported for building custom resizable UI inside your own panel content (guide →)
  • Zero extra dependencies — no runtime dependencies beyond React itself; everything is bundled in
  • TypeScript-first — complete type definitions included; no separate @types/ package needed

Installation

npm install react-dockable-desktop

Import styles in your app entry file:

import 'react-dockable-desktop/styles.css';

Requirements: React ≥ 16.8 (hooks required). No other runtime dependencies.


Quick Start

1. Create a WorkspaceClient

Define your panel catalog and create a WorkspaceClient outside React, at module scope. It acts as the bridge between your imperative code and the React tree.

// workspace.ts
import { WorkspaceClient } from 'react-dockable-desktop';
import MapPanel    from './panels/MapPanel';
import EditorPanel from './panels/EditorPanel';

export const workspace = new WorkspaceClient({
  panels: {
    map:    { component: MapPanel,    defaultOptions: { title: 'Map View' } },
    editor: { component: EditorPanel, defaultOptions: { title: 'Editor'   } },
  },
  initialState: localStorage.getItem('workspace-layout'),
});

2. Mount the Provider

DockableDesktopProvider is the single composite provider — it wraps everything the library needs:

// App.tsx
import { DockableDesktopProvider, WindowManager, ModalStackRenderer } from 'react-dockable-desktop';
import { workspace } from './workspace';

export default function App() {
  return (
    <DockableDesktopProvider client={workspace}>
      <div style={{ width: '100vw', height: '100vh' }}>
        <WindowManager />
      </div>
      <ModalStackRenderer />
    </DockableDesktopProvider>
  );
}

Important: the WindowManager container must have an explicit height. A height: 100% that resolves to zero will produce a development warning.

3. Open Panels

// From anywhere — inside or outside React:
workspace.openPanel('map-1', 'map');
workspace.openPanel('ed-1', 'editor', { title: 'config.json', initialTarget: 'floating' });
workspace.focusPanel('map-1');

// Layout persistence:
localStorage.setItem('workspace-layout', workspace.saveLayout());

// Query state without a hook:
workspace.isOpen('map-1');        // boolean
workspace.getOpenPanelIds();      // string[]

Panels can now be dragged, split, tabbed, floated, and minimized out of the box.


Writing a Panel Component

A panel is any React component. Use built-in hooks to integrate with the layout:

import { useFormContainer, usePanelId } from 'react-dockable-desktop';

export default function EditorPanel() {
  const panelId = usePanelId();               // this panel's instance ID — no prop needed
  const { setDirty, setTitle } = useFormContainer();

  const handleChange = (value: string) => {
    setDirty(true);                           // blocks close until user confirms discard
    setTitle('config.json *');                // updates the tab title live
  };

  return <textarea onChange={e => handleChange(e.target.value)} />;
}

Lifecycle callbacks

useFormContainer() exposes a full push-based lifecycle API — no subscriptions to global state required:

import { useFormContainer } from 'react-dockable-desktop';
import { useEffect } from 'react';

export default function MapPanel() {
  const {
    containerType,          // current container type at mount: 'dockable-panel' | 'floating-window'
    onActivate,             // fires when this panel becomes the globally active panel
    onDeactivate,           // fires when this panel loses active status (or is destroyed)
    onContainerTypeChange,  // fires when the panel moves between docked and floating
    onClose,                // fires just before the panel is destroyed
    requestMinimize,        // imperatively minimize this panel to the taskbar
    getDimensions,          // synchronously read current {width, height} — null until first layout
  } = useFormContainer();

  useEffect(() => {
    const unsub = [
      onActivate?.(() => {
        // e.g. resume animation, reload data
        const dims = getDimensions?.();
        console.log('active, size:', dims);
      }),
      onDeactivate?.(() => {
        // e.g. pause background work
      }),
      onContainerTypeChange?.((type) => {
        // type === 'floating-window' | 'dockable-panel'
        // e.g. trigger map.resize() after layout change
      }),
      onClose?.(() => {
        // final cleanup — unsubscribe from external stores
      }),
    ];
    return () => unsub.forEach(fn => fn?.());
  }, []);

  return <div>Map</div>;
}

Hooks

Call these inside any component within the DockableDesktopProvider tree:

| Hook | Returns | Use For | | :--- | :--- | :--- | | useWindowManagerActions() | WindowActions | Open, close, float, dock, minimize, maximize, serialize panels | | useWindowManagerState(selector?) | WindowState or selected slice | Read layout, floating windows, active panel ID | | usePanelActions() | PanelActions | Open modal overlays and left/right side drawers | | usePanelContext() | { publish, subscribe } | Inter-panel typed event bus | | useFormContainer() | FormContainerContract | Dirty state, close guards, dynamic title/icon, lifecycle callbacks (activate, deactivate, container-type change), imperative minimize, sync dimensions | | usePanelSize() | { width, height } \| null | Reactive alternative to getDimensions() — live panel dimensions across docking, floating, and tab changes, no manual subscription | | usePanelId() | string | The panel's own instance ID — no prop drilling needed | | useToolbar() | ToolbarContextValue | Read/write Toolbar state (active tool, modifiers) from any panel | | useSidebar() | SidebarContextValue | Open/close Sidebar tabs from any component in the Sidebar tree | | useSidebarTab() | SidebarTabContext | Self-control for content inside a Sidebar tab | | usePanelContextMenu(items) | void | Inject dynamic context menu items into this panel's right-click menu | | usePanelFloatingWindowManager() | PanelFloatingWindowManagerHandle | Open/close N named floating windows inside a panel overlay at runtime; each independently anchored, dockable, and resizable | | useRegistry() | PanelRegistryClass | The scoped panel registry for the current provider | | useFormatMessage() | MessageFormatter | i18n formatter matching the current provider's locale | | usePanelContribution(contribution) | void | Publish toolbar items/sidebar sections shown only while this panel is active | | useActivePanelContribution() | PanelContribution \| null | Read the active panel's published contribution, to merge manually | | useMergedToolbarItems(staticItems) | ToolbarItem[] | staticItems + the active panel's contributed toolbar items, ready for <Toolbar items={...}> | | useMergedSidebarTabs(staticTabs) | SidebarTab[] | staticTabs + the active panel's contributed sections as dynamic tabs, ready for <Sidebar tabs={...}> | | useColorScheme() | 'dark' \| 'light' | Reactively read the workspace's current color scheme from your own panel content |

State selectors prevent unnecessary re-renders:

// Only re-renders when activePanelId changes — not on every layout mutation:
const activeId = useWindowManagerState(s => s.activePanelId);
const panelCount = useWindowManagerState(s => Object.keys(s.panels).length);

WorkspaceClient Reference

const workspace = new WorkspaceClient({ panels, initialState?, formatMessage?, dir? });

// Panel lifecycle
workspace.openPanel(id, component, options?)   // options: title, initialTarget, anchor
workspace.closePanel(id)
workspace.focusPanel(id)                       // raises floating / selects tab for docked
workspace.floatPanel(id, rect?, anchor?)       // detach to a floating window; optional corner anchor
workspace.dockPanel(id)                        // return floating to the grid
workspace.minimizePanel(id)
workspace.restorePanel(id)
workspace.maximizePanel(id)

// Synchronous state queries (no hook needed)
workspace.isOpen(id)                           // → boolean
workspace.getOpenPanelIds()                    // → string[]

// Layout persistence
workspace.saveLayout()                         // → JSON string
workspace.loadLayout(json)                     // → boolean (true = success)

// Event bus
workspace.publish(event, data)
workspace.subscribe(event, callback)           // → unsubscribe()
workspace.onPanelOpen(cb)
workspace.onPanelClose(cb)
workspace.onPanelMinimize(cb)
workspace.onPanelRestore(cb)

// Direction
workspace.setDirection('ltr' | 'rtl')

FormContainerContract Reference

useFormContainer() returns a FormContainerContract with these members:

| Member | Type | Description | | :--- | :--- | :--- | | requestClose(options?) | (options?: CloseOptions) => void | Request the container to close; respects dirty state and close guards | | setDirty(dirty, options?) | (dirty: boolean) => void | Mark unsaved changes; triggers confirmation dialog on close | | onCloseRequested(handler) | (handler) => unsubscribe | Register a close guard; return false to block the close | | setTitle(title) | (title) => void | Change the tab/window title dynamically | | setIcon?(icon) | (icon: ReactNode) => void | Change the tab/window icon dynamically | | containerType? | ContainerType | Container type at mount time — see onContainerTypeChange for live updates | | instanceId | string | The panel's instance ID | | onClose?(handler) | (handler) => unsubscribe | Subscribe to panel destruction | | onMinimize?(handler) | (handler) => unsubscribe | Subscribe to minimize events | | onRestore?(handler) | (handler) => unsubscribe | Subscribe to restore-from-taskbar events | | onResize?(handler) | (handler) => unsubscribe | Subscribe to resize events; handler receives (width, height) | | requestMinimize?() | () => void | Imperatively minimize this panel to the taskbar | | getDimensions?() | () => {width, height} \| null | Synchronously read the current rendered size; null until first layout | | onActivate?(handler) | (handler) => unsubscribe | Subscribe to this panel becoming the globally active panel | | onDeactivate?(handler) | (handler) => unsubscribe | Subscribe to this panel losing active status; also fires on destruction | | onContainerTypeChange?(handler) | (handler) => unsubscribe | Subscribe to dock↔float transitions; handler receives the new ContainerType |

ContainerType

type ContainerType =
  | 'dockable-panel'   // panel is docked in the grid
  | 'floating-window'  // panel is in a detached floating window
  | 'left-panel'       // rendered inside the left side drawer
  | 'right-panel'      // rendered inside the right side drawer
  | 'modal'            // rendered inside a modal overlay
  | 'standalone';      // rendered outside the Window Manager (default / no context)

containerType on the contract reflects the state at mount time. Subscribe to onContainerTypeChange to get notified whenever the panel moves between 'dockable-panel' and 'floating-window'. Minimize/restore cycles do not fire onContainerTypeChange; use onMinimize/onRestore for those.

All on* subscribers return an unsubscribe function. Call it (or return it from useEffect) to avoid leaks.


Layout Persistence

// Save on unload (or on any meaningful user action):
window.addEventListener('beforeunload', () => {
  localStorage.setItem('workspace-layout', workspace.saveLayout());
});

// Restore by passing the saved string to the constructor:
new WorkspaceClient({
  panels: { ... },
  initialState: localStorage.getItem('workspace-layout'),
});

Side Panels & Modals

Add SidePanelRenderer and ModalStackRenderer to your app root. Placement matters — SidePanelRenderer must be inside the workspace container so drawers position correctly; ModalStackRenderer goes outside as a full-screen overlay:

// App.tsx
import { SidePanelRenderer, ModalStackRenderer } from 'react-dockable-desktop';

function App() {
  return (
    <DockableDesktopProvider client={workspace}>
      <div style={{ width: '100vw', height: '100vh', overflow: 'hidden', position: 'relative' }}>
        <WindowManager />
        <SidePanelRenderer />  {/* inside — drawers position relative to this container */}
      </div>
      <ModalStackRenderer />   {/* outside — full-screen overlay */}
    </DockableDesktopProvider>
  );
}

// From any panel component:
const { openModal, openRightPanel } = usePanelActions();

openModal(MyForm, { itemId: 42 }, { title: 'Edit Item', size: 'medium' });
openRightPanel(PropertiesPanel, { nodeId }, { title: 'Properties', width: 320 });

Touch & Mobile

Touch support is built in for v3.1.0+. No extra setup required:

  • Tab drag — long-press (300ms) on any tab to start dragging; haptic feedback on supported devices
  • Floating window drag — long-press the titlebar, then drag
  • Resize — drag any of the 8 resize handles; minimum 44px touch targets throughout
  • Split resizer — drag the 1px divider line; the hit area extends into the safe direction to avoid accidental tab activation
  • Tab bar scroll — swipe horizontally in the tab strip to scroll when there are many tabs

i18n & RTL

The library does not auto-detect direction — the consuming app owns it. Two things must be wired together:

// 1. Keep html[dir] in sync for portals (ContextMenu, flyout, Toast)
//    that render into document.body and need CSS direction inheritance.
useEffect(() => {
  document.documentElement.dir = isRtl ? 'rtl' : 'ltr';
}, [isRtl]);

// 2. Pass dir prop to the provider — controls workspace layout engine.
<DockableDesktopProvider
  dir={isRtl ? 'rtl' : 'ltr'}
  formatMessage={(msg) => intl.formatMessage({ id: msg.id, defaultMessage: msg.defaultMessage })}
  predefinedMessages={customMessages}
>

dir can be 'ltr' (default) or 'rtl'. All layout, split directions, tab ordering, floating window controls, drop zones, sidebars, and context menus flip automatically.

Direction is independent of locale — you can have Arabic translations with LTR layout, or RTL without locale changes.

See the RTL Support guide for the complete wiring pattern and macOS skin notes.


Skins

<WindowManager skin="vscode" />   // default
<WindowManager skin="macos" />
<WindowManager skin="nord" />
<WindowManager skin="tokyo" />

| Skin | Character | Active state (Sidebar & Toolbar) | |------|-----------|----------------------------------| | vscode | VS Code dark (default) | Transparent fill, 2 px accent bar | | macos | Glass Chip — accent fill, rounded corners | 36 px floating chip, white inner ring | | chrome | Google Chrome tab geometry | Sidebar: half-pill bridge. Toolbar: 2 px bar | | slate | Fluent Slate — deep navy | Floating 36 px accent-tinted pill | | nord | Arctic Frost — muted Nord palette | Short horizontal line below icon | | obsidian | Vercel Midnight — pure black/white | Deep glow + icon drop-shadow | | tokyo | Tokyo Night — purple accent | Neon glow + vivid icon drop-shadow |

All built-in skins include dark and light variants. Create your own skin by overriding CSS custom properties under a [data-workspace-skin="myskin"] selector. See the Theming Guide for the full variable reference and the Per-skin active state guide to customise the Sidebar/Toolbar active indicator in your own skin.


What's New

Every release is documented in one place — see the CHANGELOG for the full, up-to-date history of additions, fixes, and breaking changes.

Upgrading across a major version? See the Migration Guide.


Documentation

Complete guides, API reference, and interactive demo at:

https://felipecarrillo100.github.io/react-dockable-desktop/

| Guide | Description | | :--- | :--- | | Installation | Requirements, CSS import order, module formats | | Quick Start | Minimal working app with layout persistence | | WorkspaceClient | Full imperative API, multiple providers, i18n config | | Panel Registry | defaultOptions, scoped vs global registry | | Layout System | Opening, floating, minimizing, serializing layouts | | Panel Lifecycle & Forms | Dirty state, close guards, useFormContainer | | Modals & Side Panels | Modal stack, drawers, Sidebar component | | Event Bus | Typed pub/sub, built-in lifecycle events | | Theming | CSS variables, custom skins, dark/light modes | | Advanced Topics | RTL, multiple workspaces, custom header actions, custom drag-resize interactions | | Best Practices | Patterns for production-ready implementations | | Panel Overlay | PanelOverlayRoot, panel toolbars, PanelFloatingWindow, usePanelFloatingWindowManager | | Toast Notifications | toast singleton, <ToastContainer>, queue behaviour, theming, ToastAdapter | | Migration Guide | Upgrade from v1 → v2 → v3 → v4 | | API Reference | Full type-level reference for all exports |


Development

git clone https://github.com/felipecarrillo100/react-dockable-desktop.git
cd react-dockable-desktop
npm install
npm run dev          # Leaflet + Monaco open-source demo
npm run dev:ria      # LuciadRIA 3D Earth demo (requires license)
npm test             # vitest unit suite
npm run build        # build dist/

License

MIT — free to use, adapt, and build upon.


Donations & Sponsoring

Creating and maintaining open-source libraries is a passion of mine. If you find this library useful and it saves you time, please consider supporting its development. Your contributions help keep the project active and motivated!

Every bit of support—whether it's sponsoring on GitHub, a coffee, a star, or a shout-out, is deeply appreciated. Thank you for being part of the community!

Sponsor