Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Resizable

Draggable split panels on react-resizable-panels v4 — a token-driven rail that turns amber while active, with an optional grip chip and host-persisted layout.

Import

Resizable is a registry composite — you own the source after installing:

npx ploxum add resizable
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/resizable";

Depends on react-resizable-panels (v4 — Group / Panel / Separator), @roger-emerson/ploxum-icons (GripVertical), and @roger-emerson/ploxum-utils (cn).

Usage

A horizontal group of three panels, split by grip handles:

<div className="h-[320px] w-full overflow-hidden rounded-sm border border-[var(--ploxum-color-border-subtle)]">
  <ResizablePanelGroup id="workspace" orientation="horizontal">
    <ResizablePanel defaultSize="25%" minSize="15%">
      <Sidebar />
    </ResizablePanel>
    <ResizableHandle withHandle />
    <ResizablePanel defaultSize="50%">
      <Canvas />
    </ResizablePanel>
    <ResizableHandle withHandle />
    <ResizablePanel defaultSize="25%" minSize="20%">
      <Inspector />
    </ResizablePanel>
  </ResizablePanelGroup>
</div>

Nest a vertical group inside a panel — handles take their divider orientation (and grip rotation) from their own group:

<ResizablePanelGroup id="shell" orientation="horizontal">
  <ResizablePanel defaultSize="50%">
    <MapCanvas />
  </ResizablePanel>
  <ResizableHandle withHandle />
  <ResizablePanel defaultSize="50%">
    <ResizablePanelGroup id="rail" orientation="vertical">
      <ResizablePanel defaultSize="35%" minSize="20%" className="min-h-0">
        <AlertCenter />
      </ResizablePanel>
      <ResizableHandle withHandle />
      <ResizablePanel defaultSize="65%" minSize="20%" className="min-h-0">
        <EventStream />
      </ResizablePanel>
    </ResizablePanelGroup>
  </ResizablePanel>
</ResizablePanelGroup>

Persist layout on the host by reading and restoring the v4 keyed layout map. onLayoutChange fires continuously during a drag, onLayoutChanged fires once on release — both hand you a Layout map ({ [panelId]: size }). Persist on onLayoutChanged so you write storage on settle, not every frame; restore via defaultLayout:

const [layout, setLayout] = useState(() => {
  const saved = localStorage.getItem("ploxum.shell.layout");
  return saved ? JSON.parse(saved) : undefined;
});
 
<ResizablePanelGroup
  id="shell"
  orientation="horizontal"
  defaultLayout={layout}
  onLayoutChanged={(next) => {
    setLayout(next);
    localStorage.setItem("ploxum.shell.layout", JSON.stringify(next));
  }}
>
  {/* panels — each must carry a stable `id` to key into the layout map */}
</ResizablePanelGroup>

Props

ResizablePanelGroup

Wraps react-resizable-panels v4 Group (GroupProps). The wrapper sets data-slot and merges flex/orientation classes; everything else forwards through.

PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"Split axis. v4 name — not the v2 direction. Drives the aria-orientation the rail and flex-col class key off.
idstringStable group id. Keys panels into the layout map; required for persistence.
defaultLayoutLayout (Record<string, number>)Initial keyed layout — { [panelId]: size }. Restores a saved arrangement. Per-panel defaultSize alone does not initialize flex-grow; pass defaultLayout to size a group on mount.
onLayoutChange(layout: Layout) => voidFires continuously during a drag with the keyed size map.
onLayoutChanged(layout: Layout) => voidFires once on release. Use this for persistence (write storage on settle, not every frame).
classNamestringMerged after the wrapper's flex/orientation classes.

Remaining GroupProps (e.g. keyboardResizeBy, storage, style) pass straight through to v4 Group.

ResizablePanel

Wraps react-resizable-panels v4 Panel (PanelProps). The wrapper only sets data-slot and forwards className.

PropTypeDefaultDescription
idstringStable panel id — the key used in the group's layout map.
defaultSize`${number}%`Initial size as a percentage string, e.g. "25%".
minSize`${number}%`Lower bound; prevents the panel collapsing past this.
maxSize`${number}%`Upper bound.
collapsiblebooleanfalseAllow collapse to collapsedSize when dragged below minSize.
classNamestringApplied to the panel element. Use min-h-0 so inner scrollers keep bounded height.

Remaining PanelProps (order, collapsedSize, onResize, onCollapse, onExpand) pass straight through to v4 Panel.

ResizableHandle

Wraps react-resizable-panels v4 Separator (SeparatorProps) and adds one prop.

PropTypeDefaultDescription
withHandlebooleanfalseRenders the centered GripVertical chip. The grip rotates 90° automatically under a horizontal divider (vertical group).
classNamestringMerged after the rail's base + state classes.

Remaining SeparatorProps (disabled, hitAreaMargins, onClick, tabIndex) pass straight through to v4 Separator.

Design rules

  • Token-driven rail. The hairline reads --ploxum-color-border-subtle at rest and switches to --ploxum-color-amber-default on data-[separator=hover] and data-[separator=active]; keyboard focus paints --ploxum-color-border-focus. State drives the amber — never hue alone.
  • Grip is the only motion here. The rail's transition-colors and the grip chip are a spatial affordance, the sanctioned place for a flourish. It respects motion-reduce. Don't add motion to panel content.
  • Min sizes are mandatory on collapsible content. Set minSize on any panel hosting a scroller so it can't be dragged to zero and orphan its contents.
  • Bound inner scrollers. Panel content that scrolls (AlertCenter, EventStream) must sit in a min-h-0 flex context — without it the flex child grows to its content and the inner scroller loses its bounded height.
  • Layout is the host's to persist. The composite is uncontrolled by default; wire id + onLayoutChanged → storage → defaultLayout at the host when an arrangement should survive reloads.

Storybook

Composites/Resizable ↗