Resizable
Draggable, keyboard-resizable split panes — horizontal or vertical, nestable, with an optional collapsible panel. Built on react-resizable-panels.
- Status
- Since
0.1.0- Accessibility pattern
- ARIA window splitter
Last updated
Install
Add Resizable from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/resizableIt 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).
<ResizablePanelGroup orientation="horizontal">
<ResizablePanel />
<ResizableHandle withHandle />
<ResizablePanel />
</ResizablePanelGroup>ResizablePanelGroup— the flex container (data-slot="resizable-panel-group").orientation(horizontal, the default, orvertical) picks the split axis;disabledfreezes every panel and handle inside it.ResizablePanel— one resizable region (data-slot="resizable-panel"). Constrain it withminSize/maxSize/defaultSize, or make itcollapsibledown tocollapsedSize(0% by default) — pair withpanelReffor imperativecollapse()/expand()/isCollapsed().ResizableHandle— the draggable divider (data-slot="resizable-handle",role="separator"). Focusable and keyboard-operable out of the box;withHandleadds a small visible grip glyph.
Examples
Horizontal (default)
Two panels side by side. Drag the divider, or focus it (Tab) and use ← /
→.
Vertical
Set orientation="vertical" to stack panels top-to-bottom; the handle bar renders horizontal and
resizes with ↑ / ↓ instead.
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.
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.
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.
Main content
Playground
Try both split axes and the optional grip glyph — the divider stays live to drag — then copy the generated JSX.
<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-panelsv4 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 standarddivattributes and arefto the rootHTMLDivElement.
ResizablePanelGroup
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" | The split axis — panels side by side, or stacked top-to-bottom. |
disabled | boolean | false | Freezes resize interaction on every panel and handle inside the group. |
defaultLayout | Layout | — | Initial layout (a map of panel id → size) — restore a persisted layout between page loads. |
onLayoutChanged | (layout: Layout, meta: LayoutChangedMeta) => void | — | Called after a layout change settles (pointer released) — the recommended hook for persisting layouts. |
onLayoutChange | (layout: Layout) => void | — | Called continuously while the layout is changing (every pointer move) — prefer onLayoutChanged for storage. |
groupRef | Ref<GroupImperativeHandle> | — | Imperative API: getLayout() / setLayout(layout). |
id | string | number | — | Identifies 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.
| Prop | Type | Default | Description |
|---|---|---|---|
defaultSize | number | string | — | Initial size within the group; auto-assigned from the panel count when omitted. |
minSize | number | string | "0" | Lower resize bound for the panel. |
maxSize | number | string | "100%" | Upper resize bound for the panel. |
collapsible | boolean | false | Lets the panel collapse to collapsedSize when dragged past minSize. |
collapsedSize | number | string | "0" | Size the panel snaps to when collapsed. |
disabled | boolean | false | The panel cannot be resized, directly or indirectly (by resizing a neighbor). |
onResize | (size: PanelSize, id, prevSize?) => void | — | Called when the panel's size changes — size carries both asPercentage and inPixels. |
panelRef | Ref<PanelImperativeHandle> | — | Imperative API: collapse() / expand() / resize(size) / getSize() / isCollapsed(). |
id | string | number | — | Identifies the panel (also set as data-panel) — used to associate persisted layouts. Falls back to useId. |
ResizableHandle
| Prop | Type | Default | Description |
|---|---|---|---|
withHandle | boolean | false | Renders a small visible grip glyph centered on the bar — a visual affordance only; the bar is always the full drag/keyboard target. |
disabled | boolean | false | The handle can't resize its neighbors (they may still resize indirectly — disable the panels too to freeze them entirely). |
disableDoubleClick | boolean | false | Disables the double-click-to-reset-to-default-size behavior. |
id | string | number | — | Identifies the separator (also set as data-separator). Falls back to useId. |
Accessibility
ResizableHandlerendersrole="separator"witharia-valuenow/aria-valuemin/aria-valuemaxreflecting the adjacent panel's current size, andaria-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 centralizedbase.css:focus-visibleoutline, plus abg-primaryfill on hover, drag, and keyboard focus so the active target is always visually obvious. - A
disabledhandle isaria-disabledand drops out of the tab order. Adisabledgroup 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 handlearia-disabled; setdisabledon individualResizableHandles too if you need that reflected in the accessibility tree. - Give each
ResizableHandleanaria-label(e.g."Resize sidebar and content") describing what it resizes — the default accessible name is not descriptive on its own.
| Key | Action |
|---|---|
| Tab | Move focus to the handle. |
| ← / → | Resize a horizontal group's adjacent panels by a step. |
| ↑ / ↓ | Resize a vertical group's adjacent panels by a step. |
| Home | Jump the panel before the handle to its minimum size. |
| End | Jump the panel before the handle to its maximum size. |
| Contract | States tested |
|---|---|
| Behaviour | default, active, disabled |
| Accessibility | disabled, focus-visible |
| Visual | default, hover, focus, disabled |