Skip to content
Component installs need the registry setup
VegaStack Design

Resizable

Accessible resizable panel groups and layouts with keyboard support — horizontal or vertical, nestable, with an optional visible grip.

Status
stable
Since
0.1.0
Accessibility pattern
ARIA window splitter

Last updated

One
Two
Three

Install

Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Resizable before installation. Retain the digest printed by this command.

pnpm exec vegastack-design verify resizable

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/resizable

Run the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.

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";

<ResizablePanelGroup orientation="horizontal">
  <ResizablePanel>One</ResizablePanel>
  <ResizableHandle />
  <ResizablePanel>Two</ResizablePanel>
</ResizablePanelGroup>;

A group fills its parent at 100%/100%, so the parent owns the height. Sizes are unitless strings read as percentages — defaultSize="50%" and defaultSize="50" are the same thing, and a bare number is pixels.

One
Two
Three

Anatomy

ResizableHandle — data-slot="resizable-handle"
ResizablePanel — data-slot="resizable-panel"
ResizablePanelGroup — data-slot="resizable-panel-group"

Examples

About

The Resizable component is built on top of react-resizable-panels by bvaughn. The engine owns the layout maths, the pointer drag and the keyboard resize; the bar, the grip and the focus affordance are ours. We are on v4, whose API renamed PanelGroup to Group, PanelResizeHandle to Separator and direction to orientation — the three wrapper names on this page are unchanged by that.

Drag the bar,
or focus it and use the arrow keys.

Composition

A group is panels with handles between them. Every handle is a role="separator" control that resizes the two panels it sits between.

ResizablePanelGroup
├── ResizablePanel
├── ResizableHandle
└── ResizablePanel
One
Two

Vertical

orientation="vertical" stacks the panels and turns the handle into a horizontal bar. Groups nest, so a vertical group inside a horizontal panel is how the two-axis layout in Usage is built.

Header
Content

Nesting the other way round — a horizontal group inside a vertical panel — puts the inner handle's end against the outer one, so the two boundaries meet in a T.

Header
List
Detail

Handle

withHandle on ResizableHandle renders a visible grip centred on the bar. It is a visual affordance only — the whole bar remains the drag target and the keyboard target either way.

Sidebar
Content

RTL

Set dir="rtl" on the group. This component is not a Base UI part, so its reading direction comes from the attribute rather than from DirectionProvider; the handle's own offsets are already mirrored with rtl: variants.

واحد
اثنان
ثلاثة

Playground

Switch the split axis and the grip, then copy the generated JSX — including the bounded-height parent the group needs.

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

PropTypeDefaultDescription
withHandleboolean—

ResizablePanelGroup and ResizablePanel add no props of their own — each accepts everything the corresponding react-resizable-panels v4 part accepts. On the group that is orientation, disabled, defaultLayout, onLayoutChange / onLayoutChanged, groupRef and id; on a panel it is defaultSize, minSize, maxSize, collapsible, collapsedSize, disabled, onResize, panelRef and id. Both also take standard div attributes and a ref.

Accessibility

  • Each handle is a role="separator" with aria-orientation, aria-controls naming the panel it resizes, and aria-valuenow / aria-valuemin / aria-valuemax carrying that panel's current size — the ARIA window-splitter pattern.
  • A handle is a tab stop, and the arrow keys resize it; Home and End jump to the bounds. A disabled handle drops out of the tab order and reports aria-disabled.
  • Focus is the global 2px :focus-visible outline from base.css. Upstream suppressed it with focus-visible:outline-hidden and replaced it with a ring; both are removed (FOC-1, FOC-6), so a keyboard user can see which handle they are about to move.
  • The grip rendered by withHandle is decoration; the accessible target is the bar itself, which is wider than its 1px line through an after: hit area.
  • Give a handle an aria-label when a group has more than one, so the two separators are distinguishable when read out of context.
ContractStates tested
Behaviourdefault, hover, focus, dragging, disabled
Accessibilitynative-or-base-ui-semantics, browser-accessibility-test, keyboard-navigation, disabled, focus-visible
Visualdefault, focus, disabled

Do / Don't

Do
Use Resizable for a layout the reader rearranges — an editor and its preview, a list and its detail pane — and give the group's parent a real height.
Don't
Put a resizable split inside a scrolling document, or rely on the visible grip as the only cue that a boundary can move.

Deviations

Upstream's file plus packages/ui/upstream/patches/resizable.patch. Every hunk:

  • FOC-1, FOC-6 — focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-hidden goes from ResizableHandle. outline-hidden is the v3 spelling of "no outline", so upstream suppressed the one focus affordance this system has on a control that is keyboard-operable (arrow keys resize it). ring-offset-background is left alone: it paints nothing without a ring width and is upstream's own class.
  • DOC-2 — import { cn } from "cn" becomes @vegastack/design; our npm layer owns cn.
  • DOC-1 — prettier reflow plus the registry-stamp.mjs provenance header the three-copy model needs; no decision of its own.

On this page