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

biombo

v0.7.0

Published

Headless panel and window management for React: z-order, focus, Escape stack, docking, minimize and persistence.

Downloads

109

Readme

biombo

npm size license

Headless panel and window management for React: z-order, focus, Escape stack, docking, minimize and persistence.

Panels opening, taking focus, docking to the left, right and bottom edges, minimizing and closing with Escape

Live demo

biombo owns the state — stacking, focus, Escape, docking, minimize, tabs, persistence — and you own every pixel: it ships no styles, only state, events, data-*/ARIA attributes and the functional inline style (position, left/top/width/height, z-index). Docked panels shrink the usable area it reports, so your own content (a map, an editor) stays visible next to them.

Install

npm install biombo

| Entry | What it is | Needs | |---|---|---| | biombo | Framework-free core: manager, z-order bands, Escape stack, layout, persistence, anchoring | nothing | | biombo/react | BiomboProvider, usePanel and the layout, tabs and surface hooks; re-exports the core | react >= 19; react-dom >= 19 for Portal |

Quick start

import { BiomboProvider, useArea, usePanel } from 'biombo/react';

function Table() {
    const panel = usePanel('table', { dockable: ['bottom'], initial: { open: true } });
    return <section {...panel.getWindowProps()}><header {...panel.getHandleProps()}>Drag me to the bottom edge</header></section>;
}

function Map() {
    const { top, right, bottom, left } = useArea();
    return <main style={{ position: 'fixed', inset: `${top}px ${right}px ${bottom}px ${left}px` }} />;
}

export const App = () => <BiomboProvider><Map /><Table /></BiomboProvider>;

Core

import { createManager, localStorageAdapter } from 'biombo';

const manager = createManager({
    edges: { right: { policy: 'tabs' } },
    persist: { storage: localStorageAdapter(), key: 'app.panels', version: 1 },
    onEvent: (event) => sync(event),
});

manager.register('layers', { dockable: ['left', 'right'] });
manager.subscribe(() => render(manager.getSnapshot()));
manager.open('layers');
manager.dock('layers', 'right');
manager.setDockSize('layers', 320);

BiomboProvider creates a manager from options, or takes yours through manager.

Stacking and focus

Every panel lives in a named band; biombo orders panels inside each band and the band sets the base z-index. Opening or pressing a panel brings it to the front of its band and marks it active. The default bands and their base: overlay 5, docked 20, floating 30, menu 50 and modal 60. Pass bands to replace them and band per panel to choose one. A group with mode: 'single' shows one panel at a time; free (the default) lets them overlap.

Escape stack

One Escape closes only the topmost layer. Layers are ordered by priority, then by recency:

| Priority | Value | For | |---|---|---| | ESCAPE_PRIORITY.mode | 0 | Global modes, which close last (useGlobalMode) | | ESCAPE_PRIORITY.surface | 100 | Panels (the default escapePriority) | | ESCAPE_PRIORITY.menu | 200 | Menus and popovers | | ESCAPE_PRIORITY.modal | 300 | Modals |

The provider listens on window in the capture phase and stops the event it handles. Pass listenEscape={false} and call manager.escape.handle(event) to route it yourself.

Docking and the usable area

A panel docks to the edges listed in dockable. Each edge has a policy for a second panel:

| Policy | Behavior | |---|---| | replace | The new panel takes the edge; the previous one floats again (the default) | | stack | Panels share the edge, one after the other | | tabs | One panel shows at a time; useEdge and useTabs give the strip |

Docked sizes start at 40 % of the viewport, never below 120 px nor above 80 % (defaultDockRatio, minDockSize, maxDockRatio). The snapshot keeps the usable area left by the docked panels; useArea reads it and onAreaChange reports it. When the browser resizes, biombo clamps floating rects and docked sizes again. dock(id, null) floats the panel back at its last rect; if it never floated, it is centered in the area with its defaultSize, or with half the viewport capped by its docked size.

Drag, snap and resize

getHandleProps drags with left/top written in a requestAnimationFrame and saves on release; links, buttons, form fields and [data-biombo-nodrag] inside the handle do not start a drag. With snap set to auto a panel docks when the pointer reaches an edge it can dock to, and through dock(); with button, only through dock(). useSnapPreview gives the rect to draw while dragging. undockOnDrag (true or a distance in px) lets a docked panel float again when dragged. getResizerProps(direction) resizes floating panels by side or corner and docked panels along their edge.

Initial position and height

manager.register('legend', {
    initial: { open: true, rect: { anchor: 'bottom-right', x: 16, y: 16, width: 260, height: 200 } },
});
manager.fitHeight('legend', contentHeight, { maxRatio: 0.6 });

initial.rect takes a plain rect or one anchored to a corner (top-left, top-right, bottom-left, bottom-right): x and y (0 by default) are the distances from that corner's edges of the usable area. It is resolved once, when the panel opens with a known viewport; a stored rect wins, and after that it is a plain rect that the user moves.

fitHeight(id, height, options) sets a floating panel's height, clamped between min (and the panel's minSize) and the lowest of max px and maxRatio of the viewport height. It keeps the top edge, or the bottom one when the panel was anchored to a bottom corner (grow: 'down' | 'up' overrides it), reports resize, and does nothing on docked or maximized panels or when nothing changes.

Presentation

state.presentation says how to draw the panel: hidden, floating, docked, maximized, or the panel's compact mode (sheet, fullscreen, inline) below compactBreakpoint. Minimizing only keeps the state: draw the pill yourself from the panels that are open and minimized (in React, getRestoreProps gives its button). contain: 'full' keeps a floating panel fully on screen instead of partially, containMargin px from the edges.

Persistence and events

persist saves minimized, dock, dockSize and rect by default (list open in fields to keep it too), under a key and a version; migrate upgrades older data. serialize and hydrate let you store the layout elsewhere. onEvent reports open, close, focus, minimize, restore, maximize, unmaximize, dock, undock, resize, move and reorder, each with id and group.

React

import { BiomboProvider, localStorageAdapter, usePanel } from 'biombo/react';

function Table() {
    const panel = usePanel('table', { dockable: ['bottom'], defaultSize: { width: 520, height: 280 } });
    return (
        <>
            <button onClick={() => panel.toggle()}>Table</button>
            <section {...panel.getWindowProps()} className="your-styles">
                <header {...panel.getHandleProps()}>Drag me</header>
                <div {...panel.getResizerProps()} />
            </section>
        </>
    );
}

const options = { persist: { storage: localStorageAdapter(), key: 'app.panels', version: 1 } };

export const App = () => <BiomboProvider options={options}><Table /></BiomboProvider>;

| Hook | For | |---|---| | usePanel | State, actions and prop getters of one panel: getWindowProps, getHandleProps, getResizerProps(direction?), getRestoreProps | | useAutoHeight | Ref for the scrolling body of a panel whose height follows its content | | useEdge, useTabs | The panels of an edge and its tab strip, with keyboard and drag reorder (onReorder), controlled or not | | useCompact | Whether compact mode is on, below compactBreakpoint | | useClearance, useObstacle | Offset that keeps a floating control clear of panels and registered obstacles |

function Legend() {
    const panel = usePanel('legend', { minimizable: true, initial: { open: true, rect: { anchor: 'bottom-right', x: 16, y: 16, width: 260, height: 200 } } });
    const body = useAutoHeight('legend', { maxRatio: 0.6 });
    return (
        <>
            <section {...panel.getWindowProps()}>
                <header {...panel.getHandleProps()}>Legend</header>
                <div ref={body} style={{ overflow: 'auto' }}>…</div>
            </section>
            <button {...panel.getRestoreProps()}>Legend</button>
        </>
    );
}

useAutoHeight(id, options) measures the window's chrome plus the natural height of the body's children (in-flow only, with margins, padding and gap) and calls fitHeight when the body, its children or the list of children change, and after a drag or resize by hand ends. enabled: false stops it; neededHeight(body) is the same measure for your own code. getRestoreProps(input?) is a button that restores the minimized panel and is hidden otherwise, so it can stay mounted.

F6 (cycleKey) moves focus between open panels, and closing a panel returns focus to the element that opened it.

Surfaces

These hooks work with or without BiomboProvider:

| Hook | Does | |---|---| | useFocusTrap | Focus trap with stacked traps, inert for the rest of the page and focus returned on close | | useScrollLock | Body scroll lock that survives nested modals | | useDismissOutside | Dismiss on a press outside, topmost surface only | | useAnchoredPosition, placeAnchored | Anchored to an element or a point, flipping and clamped to the viewport; over centers on it | | useDraggable | Drag any element inside the viewport, alone or on top of an anchored surface | | useDismissGesture | Swipe to dismiss, driven by CSS variables instead of re-renders | | Portal | Render into document.body, SSR-safe |

A modal combines four of them:

import { ESCAPE_PRIORITY, Portal, useDismissOutside, useEscape, useFocusTrap, useScrollLock } from 'biombo/react';

function Modal({ onClose, children }) {
    const ref = useRef(null);
    const { getTrapProps } = useFocusTrap(ref);
    const { getDismissProps } = useDismissOutside(ref, onClose);
    useScrollLock();
    useEscape(onClose, { priority: ESCAPE_PRIORITY.modal });
    return (
        <Portal>
            <div ref={ref} {...getTrapProps()} {...getDismissProps()} role="dialog" aria-modal="true">{children}</div>
        </Portal>
    );
}

A tooltip anchors to its trigger; an info box anchors to a map click and can be dragged away:

const anchored = useAnchoredPosition(tipRef, { anchor: triggerRef, open, placement: 'top' });
<div ref={tipRef} {...anchored.getFloatingProps()} role="tooltip" />

const drag = useDraggable(boxRef, { margin: 10 });
const anchored = useAnchoredPosition(boxRef, { anchor: { x, y }, shift: drag.offset, paused: drag.dragging });
<div ref={boxRef} {...anchored.getFloatingProps()}><header {...drag.getHandleProps()} /></div>

useDismissGesture writes --biombo-swipe (px) and --biombo-swipe-progress (0–1) on the element and sets data-swipe to idle, moving, armed or dismissed; the animation is yours:

.card { transform: translateX(var(--biombo-swipe, 0px)); opacity: calc(1 - var(--biombo-swipe-progress, 0) * 0.6); }
.card[data-swipe='idle'], .card[data-swipe='dismissed'] { transition: transform 0.22s, opacity 0.22s; }

Notes:

  • position: fixed is relative to the nearest ancestor with transform, filter or contain, not to the viewport. Render anchored surfaces through Portal (this needs react-dom, an optional peer).
  • In a registered panel, pass returnFocus: false to useFocusTrap: closing the panel already returns focus to its trigger.
  • useAnchoredPosition owns the inline max-height and max-width of the surface.
  • A surface rendered through a portal inside another one counts as inside it for useDismissOutside when the outer surface spreads getDismissProps().

Known limitations

  • Panels move and resize with the pointer only; keyboard move and resize are planned after 1.0.
  • In compact mode there is no drag, snap, undock or resizer. The dock is kept and applies again above the breakpoint.
  • A hidden panel gets hidden and display: none inline, so your own display on it does not apply while it is closed.
  • Without a provider, useEscape uses a global stack. If the provider and layers outside it share a page, the listener registered first wins: put BiomboProvider at the root.
  • There is no fully controlled state prop: sync through serialize, hydrate, onEvent and, for tabs, onReorder.

Development

npm test
npm run lint
npm run typecheck
npm run demo
npm run e2e

The demo is a playground over a fake map: five panels that float, dock to every edge with stack and tabs policies, minimize to a pill bar and report the usable area, plus a modal, tooltips and an info box anchored to map clicks that can be dragged or swiped away. The e2e suite runs Playwright against it.

Contributing

Issues are welcome. Open one before sending a pull request.

License

AGPL-3.0-only © GHEgar