Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Resizable

Draggable, keyboard-resizable split panes — horizontal or vertical, nestable, with an optional collapsible panel. Built on react-resizable-panels.

Status
stable
Since
0.1.0
Accessibility pattern
ARIA window splitter

Last updated

Sidebar
Content

Install

Add Resizable from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/resizable

It also adds the sanctioned engine to your package.json: react-resizable-panels (resizable layout engine).

Usage

import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from "@/components/ui/resizable";

<div className="h-64">
  <ResizablePanelGroup className="rounded-lg border">
    <ResizablePanel defaultSize="30" minSize="20">
      Sidebar
    </ResizablePanel>
    <ResizableHandle withHandle />
    <ResizablePanel>Content</ResizablePanel>
  </ResizablePanelGroup>
</div>;

Give the group's parent an explicit height (or width, for a vertical group's cross axis) — the group fills its parent at height: 100%; width: 100% via its own inline styles, so an h-* class on the group itself is ignored and panels collapse to zero size without a bounded wrapper.

Sizes are unitless strings, not numbers. defaultSize/minSize/maxSize/collapsedSize treat a bare number (defaultSize={30}) as pixels — for a percentage, pass a string: defaultSize="30" (30%) or defaultSize="30%". Other CSS units (px, em, rem, vh, vw) are also accepted as explicit string suffixes.

Anatomy

Resizable is three flat, unstyled-by-default parts composing the headless react-resizable-panels engine — no Base UI here (the one other sanctioned exception besides MessageScroller; Base UI has no split-pane primitive).

ResizableHandle — data-slot="resizable-handle"
ResizablePanel — data-slot="resizable-panel"
ResizablePanelGroup — data-slot="resizable-panel-group"
<ResizablePanelGroup orientation="horizontal">
  <ResizablePanel />
  <ResizableHandle withHandle />
  <ResizablePanel />
</ResizablePanelGroup>
  • ResizablePanelGroup — the flex container (data-slot="resizable-panel-group"). orientation (horizontal, the default, or vertical) picks the split axis; disabled freezes every panel and handle inside it.
  • ResizablePanel — one resizable region (data-slot="resizable-panel"). Constrain it with minSize/maxSize/defaultSize, or make it collapsible down to collapsedSize (0% by default) — pair with panelRef for imperative collapse()/expand()/isCollapsed().
  • ResizableHandle — the draggable divider (data-slot="resizable-handle", role="separator"). Focusable and keyboard-operable out of the box; withHandle adds a small visible grip glyph.

Examples

Horizontal (default)

Two panels side by side. Drag the divider, or focus it (Tab) and use /

.

Sidebar
Content

Vertical

Set orientation="vertical" to stack panels top-to-bottom; the handle bar renders horizontal and resizes with / instead.

Header
Body

Nested groups

Nest a ResizablePanelGroup inside a ResizablePanel to combine axes — here the right column splits vertically, independent of the horizontal split against the sidebar.

Sidebar
Preview
Console

With a grip handle

withHandle renders a small GripVertical glyph centered on the bar — the bar itself is always the full drag/keyboard target either way, so this is a visual affordance only.

Left
Right

Collapsible panel

collapsible lets a panel collapse to collapsedSize (0% here) once it's dragged past minSize. Pair with panelRef to drive collapse() / expand() imperatively — e.g. a "hide sidebar" button — and onResize to keep other UI (the button label, the panel's own label) in sync with the collapsed state.

Sidebar

Main content

Playground

Try both split axes and the optional grip glyph — the divider stays live to drag — then copy the generated JSX.

One
Two
<div className="h-48">
  <ResizablePanelGroup className="rounded-lg border">
    <ResizablePanel defaultSize="40" minSize="20">One</ResizablePanel>
    <ResizableHandle aria-label="Resize panels" />
    <ResizablePanel minSize="20">Two</ResizablePanel>
  </ResizablePanelGroup>
</div>

API Reference

Each part extends a react-resizable-panels v4 type (GroupProps / PanelProps / SeparatorProps) that the docs type extractor can't introspect — so the rows below are hand-maintained and kept in sync with the engine. All parts also accept standard div attributes and a ref to the root HTMLDivElement.

ResizablePanelGroup

PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"The split axis — panels side by side, or stacked top-to-bottom.
disabledbooleanfalseFreezes resize interaction on every panel and handle inside the group.
defaultLayoutLayoutInitial layout (a map of panel id → size) — restore a persisted layout between page loads.
onLayoutChanged(layout: Layout, meta: LayoutChangedMeta) => voidCalled after a layout change settles (pointer released) — the recommended hook for persisting layouts.
onLayoutChange(layout: Layout) => voidCalled continuously while the layout is changing (every pointer move) — prefer onLayoutChanged for storage.
groupRefRef<GroupImperativeHandle>Imperative API: getLayout() / setLayout(layout).
idstring | numberIdentifies the group (also set as data-group). Falls back to useId.

ResizablePanel

Sizes (defaultSize / minSize / maxSize / collapsedSize) are unitless strings for percentages ("30" = 30%) — a bare number is pixels; explicit px/em/rem/vh/vw suffixes are also accepted.

PropTypeDefaultDescription
defaultSizenumber | stringInitial size within the group; auto-assigned from the panel count when omitted.
minSizenumber | string"0"Lower resize bound for the panel.
maxSizenumber | string"100%"Upper resize bound for the panel.
collapsiblebooleanfalseLets the panel collapse to collapsedSize when dragged past minSize.
collapsedSizenumber | string"0"Size the panel snaps to when collapsed.
disabledbooleanfalseThe panel cannot be resized, directly or indirectly (by resizing a neighbor).
onResize(size: PanelSize, id, prevSize?) => voidCalled when the panel's size changes — size carries both asPercentage and inPixels.
panelRefRef<PanelImperativeHandle>Imperative API: collapse() / expand() / resize(size) / getSize() / isCollapsed().
idstring | numberIdentifies the panel (also set as data-panel) — used to associate persisted layouts. Falls back to useId.

ResizableHandle

PropTypeDefaultDescription
withHandlebooleanfalseRenders a small visible grip glyph centered on the bar — a visual affordance only; the bar is always the full drag/keyboard target.
disabledbooleanfalseThe handle can't resize its neighbors (they may still resize indirectly — disable the panels too to freeze them entirely).
disableDoubleClickbooleanfalseDisables the double-click-to-reset-to-default-size behavior.
idstring | numberIdentifies the separator (also set as data-separator). Falls back to useId.

Accessibility

  • ResizableHandle renders role="separator" with aria-valuenow / aria-valuemin / aria-valuemax reflecting the adjacent panel's current size, and aria-orientation (the axis perpendicular to the group — a vertical bar between horizontally arranged panels, per the ARIA separator role).
  • The handle is in the tab order (tabIndex={0}) and keyboard-operable with no extra wiring — see the table below. It gets the centralized base.css :focus-visible outline, plus a bg-primary fill on hover, drag, and keyboard focus so the active target is always visually obvious.
  • A disabled handle is aria-disabled and drops out of the tab order. A disabled group blocks resize interaction on every panel/handle inside it (pointer and keyboard) but — since that is the underlying engine's own behavior — does not itself mark each handle aria-disabled; set disabled on individual ResizableHandles too if you need that reflected in the accessibility tree.
  • Give each ResizableHandle an aria-label (e.g. "Resize sidebar and content") describing what it resizes — the default accessible name is not descriptive on its own.
KeyAction
TabMove focus to the handle.
/ Resize a horizontal group's adjacent panels by a step.
/ Resize a vertical group's adjacent panels by a step.
HomeJump the panel before the handle to its minimum size.
EndJump the panel before the handle to its maximum size.
ContractStates tested
Behaviourdefault, active, disabled
Accessibilitydisabled, focus-visible
Visualdefault, hover, focus, disabled

Do / Don't

Do
Set minSize/maxSize on every panel so a drag or keyboard resize can't collapse content to an unusable sliver — and give every ResizableHandle a descriptive aria-label.
Don't
Forget the group's own bounded height/width, or pass a bare number expecting a percentage — defaultSize={30} is 30 pixels, not 30%.

On this page