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

v7.7.4

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

Using Vue? vue-dockable-desktop is the Vue 3 port — a native Vue library, not a wrapper, and it reads and writes the same serialised layout format, so a layout saved by either library loads in the other.


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 (RddPanelToolbar, RddToolbarButton, RddToolbarToggle, async search) and corner-anchored floating windows that stack, drag, and dock; an axis can span the panel instead of carrying a fixed size, so a strip or column tracks the panel as it resizes — set declaratively or by dragging an edge out until it snaps; useFloatingWidgets() 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 API — the workspace from createWorkspace() 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 Primitives — startPointerDrag() 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 ≥ 18, and a browser with CSS color-mix() (Chrome/Edge 111, Safari 16.2, Firefox 113 or later). No other runtime dependencies.


Quick Start

1. Create a workspace

Define your panel catalog and create the workspace with createWorkspace() outside React, at module scope. It acts as the bridge between your imperative code and the React tree, and it is live immediately — calls made before the provider mounts apply straight away.

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

export const workspace = createWorkspace({
  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 only provider — it wraps everything the library needs:

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

export default function App() {
  return (
    <DockableDesktopProvider workspace={workspace}>
      <div className="rdd-fill-viewport">
        <RddDesktop />
      </div>
      <RddModals />
    </DockableDesktopProvider>
  );
}

Important: the RddDesktop container must have an explicit height. A height: 100% that resolves to zero will produce a development warning. The stylesheet styles nothing outside the library's own elements, so rdd-fill-viewport (full-window height, no overflow) is opt-in — and remove the browser's default body margin in your own CSS (body { margin: 0 }).

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 { usePanel } from 'react-dockable-desktop';

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

  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

Lifecycle hooks are called at the top level of the panel component. They always call the latest function you pass (no dependency array) and clean up on unmount:

import { usePanel, usePanelEvents, usePanelSize, useBeforeClose, useSaveState } from 'react-dockable-desktop';

export default function MapPanel() {
  const { containerType, minimize } = usePanel();  // containerType: 'dockable-panel' | 'floating-window' | …, updates live
  const size = usePanelSize();                     // { width, height } | null — re-renders on resize

  usePanelEvents({
    onActivate:   () => { /* e.g. resume animation, reload data */ },
    onDeactivate: () => { /* e.g. pause background work */ },
    onContainerTypeChange: (type) => { /* e.g. trigger map.resize() after layout change */ },
    onClose:      () => { /* final cleanup — unsubscribe from external stores */ },
  });

  useBeforeClose(async () => confirm('Close the map?'));   // resolve false to keep it open
  useSaveState(() => ({ zoom: currentZoom() }));           // saved with the layout by saveLayout()

  return <div>Map</div>;
}

Hooks

Call these inside any component within the DockableDesktopProvider tree:

| Hook | Returns | Use For | | :--- | :--- | :--- | | useWorkspace() | Workspace | The workspace: open, close, float, dock, minimize, maximize, serialize panels; publish/subscribe; registry | | useWorkspaceState(selector?) | WorkspaceState or selected slice | Read layout, floating windows, active panel ID | | useModals() | ModalsApi | Open, close and track modal overlays | | useSidePanels() | SidePanelsApi | Open, close and track the left/right side drawers | | usePanel() | PanelHandle | Inside a panel: its id, live containerType, isActive, dirty state, dynamic title/icon, close(), minimize() | | usePanelEvents(events) | void | Inside a panel: activate, deactivate, minimize, restore, close, resize, container-type change | | useBeforeClose(guard) | void | Inside a panel: a close guard; resolve false to keep it open | | useSaveState(getState) | void | Inside a panel: state pulled fresh by every saveLayout() | | usePanelSize() | { width, height } \| null | Live panel dimensions across docking, floating, and tab changes, no manual subscription | | useToolbar() | ToolbarContextValue | Read/write toolbar state (active tool, modifiers) from any panel | | useSidebar() | SidebarContext | Open/close sidebar tabs from any component in the RddSidebar tree | | useSidebarTab() | SidebarTabContext | Self-control for content inside a sidebar tab | | useContextMenu() | (options) => void | Show the shared context menu at a pointer event or position | | usePanelContextMenu(items) | void | Inject dynamic context menu items into this panel's right-click menu | | useFloatingWidgets() | FloatingWidgetsApi | Open/close N named floating widgets inside a panel overlay at runtime; each independently anchored, dockable, and resizable | | useFormatMessage() | MessageFormatter | i18n formatter matching the current provider's locale | | useMessages() | Record<MessageKey, MessageDescriptor> | The effective built-in message table | | useHostClasses() | HostClasses | The class props set on the provider (modalClass, windowClass, …) | | usePanelContribution(contribution) | void | Publish toolbar items/sidebar sections shown only while this panel is active | | useActiveContribution() | 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 <RddToolbar items={...}> | | useMergedSidebarTabs(staticTabs) | SidebarTab[] | staticTabs + the active panel's contributed sections as dynamic tabs, ready for <RddSidebar tabs={...}> | | useColorScheme() | 'dark' \| 'light' | Reactively read the color scheme — the data-color-scheme your app sets on <html> (dark when absent) — from your own panel content |

State selectors prevent unnecessary re-renders:

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

Workspace Reference

const workspace = createWorkspace({
  panels, initialState?, formatMessage?, messages?, dir?,
  defaultSplitRatio?, defaultEdgeSplitRatio?, zIndexBase?,
});

// Panel lifecycle
workspace.openPanel(id, component, options?)   // options: title, initialTarget, anchor, focus (default true), props, dedupeKey
workspace.closePanel(id)                       // closes immediately — no guard, no dirty check
workspace.requestClosePanel(id, { force?, onConfirm? })  // guarded close; a dirty panel closes only if onConfirm resolves true
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, { focus? })
workspace.maximizePanel(id)                    // toggles a floating window; a minimized panel is restored and maximized
workspace.closeLeafGroup(leafId, opts?)        // closes each tab (guards apply), then the group; returns a Promise

// Placement
workspace.dockPanelToGroup(id, leafId, position)   // position: 'top' | 'bottom' | 'left' | 'right' | 'center'
workspace.dockPanelToWorkspaceEdge(id, side)       // 'top' | 'bottom' | 'left' | 'right'
workspace.movePanelOrder(id, leafId, index)        // move a tab within or between groups

// Title, icon and dirty state (from outside the panel; inside it, use usePanel())
workspace.updatePanelTitle(id, title)
workspace.setPanelIcon(id, icon)               // null restores the registration's icon; never saved
workspace.setPanelDirty(id, dirty, options?)

// Synchronous state queries (no hook needed)
workspace.isOpen(id)                           // → boolean
workspace.getOpenPanelIds()                    // → string[]
workspace.findPanelId(component, dedupeKey)    // → id | null

// 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)
workspace.onLayoutChanged(cb)
workspace.onPanelsExcluded(cb)                 // saveLayout() left out non-serializable panels

// Direction and menus
workspace.setDirection('ltr' | 'rtl')
workspace.showContextMenu({ x, y, items })     // the shared menu, opened from code

Lower-level methods (close guards, state providers, split and floating-window geometry) are listed in the Workspace guide.


PanelHandle Reference

usePanel() returns a PanelHandle — in docked panels, floating windows, modals and side drawers alike. Its actions never change identity; the handle object does (it carries live state), so depend on the actions, never on the handle, in dependency arrays:

| Member | Type | Description | | :--- | :--- | :--- | | id | string | The panel's instance ID | | containerType | ContainerType | Where it is rendered; updates live (a docked panel that is floated re-renders as 'floating-window') | | isActive | boolean | The globally active panel. Always false in a modal or drawer | | isMinimized / isFloating | boolean | Current state of a workspace panel | | close(options?) | (options?: CloseOptions) => void | Request the container to close; respects dirty state and close guards ({ force: true } skips them) | | minimize() | () => void | Minimize this panel to the taskbar; no effect in a modal or drawer | | setDirty(dirty, options?) | (dirty: boolean, options?: DirtyStateOptions) => void | Mark unsaved changes; triggers confirmation dialog on close | | setTitle(title) | (title: string \| MessageDescriptor \| (() => string)) => void | Change the tab/window title dynamically | | setIcon(icon) | (icon: ReactNode) => void | Change the tab, floating title bar and taskbar icon (or a modal's or drawer's header icon); null restores the registration's defaultOptions.icon. Not saved by saveLayout() |

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 desktop (default / no context)

Minimizing and restoring don't fire onContainerTypeChange; use usePanelEvents({ onMinimize, onRestore }) for those. (While minimized, a panel's containerType reads 'dockable-panel' — also for a panel that was floating, which reads 'floating-window' again once restored; check usePanel().isMinimized if you need to tell.)


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 createWorkspace():
createWorkspace({
  panels: { ... },
  initialState: localStorage.getItem('workspace-layout'),
});

Side Panels & Modals

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

// App.tsx
import { DockableDesktopProvider, RddDesktop, RddSidePanels, RddModals } from 'react-dockable-desktop';

function App() {
  return (
    <DockableDesktopProvider workspace={workspace}>
      <div className="rdd-fill-viewport" style={{ position: 'relative' }}>
        <RddDesktop />
        <RddSidePanels />  {/* inside — drawers position relative to this container */}
      </div>
      <RddModals />        {/* outside — full-screen overlay */}
    </DockableDesktopProvider>
  );
}

// From any component inside the provider:
const modals = useModals();
const sidePanels = useSidePanels();

modals.open(MyForm, { itemId: 42 }, { title: 'Edit Item', size: 'medium' });
sidePanels.openRight(PropertiesPanel, { nodeId }, { title: 'Properties', width: 320 });

Touch & Mobile

Touch support is built in. 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 the workspace's direction — the consuming app owns it. Two things must be wired together:

// 1. Keep html[dir] in sync for the rest of your page (your own chrome around the desktop).
useEffect(() => {
  document.documentElement.dir = isRtl ? 'rtl' : 'ltr';
}, [isRtl]);

// 2. Pass dir to the provider — the workspace, and the menus and flyouts opened from it.
<DockableDesktopProvider
  dir={isRtl ? 'rtl' : 'ltr'}
  workspace={workspace}
  formatMessage={(msg) => intl.formatMessage({ id: msg.id, defaultMessage: msg.defaultMessage })}
  messages={customMessages}
>

dir can be 'ltr' (default) or 'rtl'. The workspace's layout, split directions, tab ordering, floating window controls, drop zones and context menus flip automatically; so do RddSidebar (with everything inside it) and the toasts (since 7.4.0). While the workspace is left-to-right those two follow the page's dir.

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

<RddDesktop skin="vscode" />   // default
<RddDesktop skin="macos" />
<RddDesktop skin="nord" />
<RddDesktop 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, and each brings its own font (the platform's UI font where it has a known one: VS Code's, San Francisco, Google's, Fluent's Segoe UI).

Branding. Put your company's colour and font on any built-in skin — no skin of your own needed:

:root {
  --rdd-brand-accent: #e4002b;                 /* every accent use, in every skin, dark and light */
  --rdd-brand-on-accent: #ffffff;              /* text on a brand-coloured fill — set a dark one for light brands */
  --rdd-font-family: 'Acme Sans', sans-serif;  /* your font (the library loads none) */
}

Point them at your UI framework's theme to follow it: var(--bs-primary) (Bootstrap), var(--mui-palette-primary-main) (MUI with CSS variables), var(--mat-sys-primary) (Angular Material 3). See Brand your app.

Your own surfaces and corner shape, too (7.3.0):

:root:not([data-color-scheme="light"]) {  /* dark: the attribute's absence */
  --rdd-brand-surface: #0b1f3a;   /* app background — panels, bars and borders are derived from it */
  --rdd-brand-text: #e8eef7;      /* main text — set both, or neither */
}
:root { --rdd-radius-scale: 0; }  /* 0 square · 1 each skin's own · 1.5 rounder */

Create your own skin by overriding CSS custom properties under a [data-rdd-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. What may change in which release is in STABILITY.md.


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 | | Workspace | createWorkspace, full imperative API, multiple providers, i18n config | | Panel Registry | defaultOptions, per-workspace registry | | Layout System | Opening, floating, minimizing, serializing layouts | | Panel Lifecycle & Forms | Dirty state, close guards, usePanel and lifecycle hooks | | Modals & Side Panels | Modal stack, drawers, RddSidebar 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 | RddPanelOverlay, panel toolbars, RddFloatingWidget, useFloatingWidgets | | Toast Notifications | toast singleton, <RddToasts>, queue behaviour, theming, ToastAdapter | | Migration Guide | Upgrading across major versions | | Stability & Versioning | What the public API is, deprecation and support periods | | 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. See LICENSE.


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