Resizable
Split panes with pointer and keyboard resizing, pixel or percent limits, collapsible panels and a hairline handle. No third-party dependency.
On this page
Nested horizontal and vertical splits with pixel minimums.
Installation
Usage
import { ResizableGroup, ResizableHandle, ResizablePanel } from "@/components/ui/chatcn/resizable"
<ResizableGroup direction="horizontal" autoSaveId="workspace">
<ResizablePanel id="chat" defaultSize={55} minSize="320px">
<Conversation />
</ResizablePanel>
<ResizableHandle />
<ResizablePanel id="artifact" defaultSize={45} minSize="360px" collapsible>
<ArtifactPanel />
</ResizablePanel>
</ResizableGroup>Sizes are percentages of the group. Pass a string like "360px" for pixel limits; they are re-resolved whenever the group resizes, so a panel never drops below its pixel minimum on a narrow window. Panels can mount and unmount: a new panel takes its defaultSize, the others share the rest, and a panel that comes back returns at its last width.
Examples
Collapsible
A collapsible panel snaps shut once it is dragged past half its minimum. Enter on the handle toggles it, and panelRef gives you collapse(), expand() and resize() for a toolbar button.
A sidebar that collapses by drag, Enter or a button.
Props
ResizableGroup
| Prop | Type | Notes |
|---|---|---|
direction | "horizontal" | "vertical" | Defaults to horizontal |
onLayout | (sizes: number[]) => void | Percentages in DOM order, after every change |
autoSaveId | string | Persists the layout in localStorage |
ResizablePanel
| Prop | Type | Notes |
|---|---|---|
id | string | Stable id for persistence and aria-controls. Generated when omitted |
defaultSize | number | "Npx" | Size on mount and on double-click reset |
minSize, maxSize | number | "Npx" | Percent or pixels |
collapsible | boolean | Snaps shut past half the minimum |
collapsedSize | number | "Npx" | Defaults to 0 |
onCollapsedChange | (collapsed: boolean) => void | |
panelRef | Ref<ResizablePanelHandle> | collapse, expand, resize, isCollapsed, getSize |
ResizableHandle
| Prop | Type | Notes |
|---|---|---|
withGrip | boolean | Shows a small grip on the hairline |
keyboardStep | number | Percent per arrow press, default 2. Shift moves five times as far |
disabled | boolean | |
aria-label | string | Defaults to Resize panels |
Keyboard
| Key | Action |
|---|---|
← → (or ↑ ↓ when vertical) | Resize by keyboardStep. Mirrored in RTL |
Shift + arrow | Resize five times as far |
Home, End | Jump to the smallest or largest size the pair allows |
Enter | Collapse or restore the collapsible neighbor |
Accessibility
- The handle is a focusable
separatorwitharia-valuenow,aria-valuemin,aria-valuemaxandaria-controls, the APG window splitter pattern. - The range it reports already accounts for the panel across it, so screen readers hear the real limits.
- The hit area is 9 px wide with a mouse and 24 px with a finger, while the visible line stays 1 px.
Motion
- The hairline thickens to 3 px with a scale transform after a 100 ms hover intent, and at once while dragging or focused.
- While dragging, one cursor applies everywhere and iframes stop catching the pointer. At a limit the cursor turns into a one-way arrow.
- Resizing itself never animates: keyboard and drag changes are instant.