Resizable
Resizable panel groups: nested, collapsible, keyboard-driven, saved to localStorage. Grips light up as tubes.
acme-web
page.tsx
layout.tsx
billing.tsx
route.ts
schema.prisma
12 export async function updatePlan(id: string, plan: Plan) {
13 const seat = await db.seat.count({ where: { orgId: id } });
14 if (seat > limits[plan]) throw new SeatLimitError(seat);
15 return stripe.subscriptions.update(id, { plan });
16 }
$ pnpm test billing
✓ 14 passed (1.82s)
Installation
npx shadcn@latest add https://neonkit.heyaadi.com/r/resizable.jsonUsage
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"
<ResizablePanelGroup direction="horizontal" autoSaveId="editor">
<ResizablePanel defaultSize={25} minSize={15} collapsible>Files</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={75}>Editor</ResizablePanel>
</ResizablePanelGroup>Examples
Collapse to a rail
collapsedSize leaves the folder pane as an icon rail rather than hiding it.
Imperative and onLayout
A button folds the outline through apiRef; onLayout reports every size.
API reference
ResizablePanelGroup
A flex container. Panels and handles must be its direct children. Accepts every native div attribute.
| Prop | Type | Default |
|---|---|---|
direction | "horizontal"Which way the panels sit. | "horizontal" |
onLayout | (sizes: number[]) => voidEvery panel's size in percent, after each change. | None |
autoSaveId | stringSaves the layout in localStorage and restores it on the next visit. Blocked or full storage is ignored. | None |
keyboardStep | numberPercent per arrow key. Shift moves 1%. | 5 |
color | NeonColorPalette name or hex for the handles. Falls back to --neon-accent, then cyan. | inherited |
ResizablePanel
Accepts every native div attribute. Sizes are percentages of the group.
| Prop | Type | Default |
|---|---|---|
defaultSize | numberStarting size. Panels without one share what's left. Set it on every panel to avoid a jump after hydration. | None |
minSize | numberSmallest size while open. | 0 |
maxSize | numberLargest size. | 100 |
collapsible | booleanCan fold to collapsedSize: drag past half the minimum, double-click the handle or press Enter on it. | false |
collapsedSize | numberSize when collapsed, e.g. room for an icon rail. | 0 |
onResize | (size: number) => voidCalled when this panel's size changes. | None |
onCollapse / onExpand | () => voidCalled when the panel folds or opens. | None |
apiRef | Ref<ResizablePanelApi>collapse(), expand(), resize(size), getSize() and isCollapsed(). | None |
ResizableHandle
A focusable separator. Give it an aria-label that names what it resizes.
| Prop | Type | Default |
|---|---|---|
withHandle | booleanShows the grip. Without it the handle is a hairline that lights up. | true |
disabled | booleanCan't be dragged or focused. | false |
Accessibility
- Handles follow the APG window splitter: role separator, aria-valuenow is the size of the panel before it, and aria-controls points at that panel.
- Arrow keys resize along the group's direction, Shift for 1%. Home and End go to the minimum and maximum.
- Enter folds the panel before the handle, or the one after it if only that one is collapsible, and Enter again restores its last size.
- A shrinking panel that reaches its minimum passes the rest of the drag to the next one along.
- The grip lights as a tube while hovered, focused or dragged; at rest it's an unlit glass capsule.
- Folding with the keyboard or a double-click animates; dragging never does, and reduced motion turns the animation off.