Resizable
Drag-to-adjust panel groups. Use when you want users to control column widths — for static layouts with fixed-width columns, prefer AppShell with nav="three-pane" (no JS, server-renderable).
Installation
import {
ResizablePanelGroup,
ResizablePanel,
ResizableHandle,
} from "@gradeui/ui"Built on react-resizable-panels — same API, gradeui styling and tokens.
Horizontal split
Two columns split 30 / 70. Drag the divider to resize. Hover over the divider to see the hit area.
Visible handle
withHandle renders a small grip in the middle of the divider so it's easier to spot. Use this for general audiences; skip it for power-user tools where every pixel matters.
Three-pane workspace
Slack/Mail/Notion shape with adjustable middle column. Each panel has minSize so users can't crush a pane to nothing.
Vertical split
Set direction="vertical" to stack panels with horizontal drag handles between them — useful for an editor + console layout.
Nested groups
Panels can themselves contain a ResizablePanelGroup — useful when one column should split top-to-bottom while another stays single. Nest groups freely; sizes resolve independently per group.
Persisting layout
Pass id to the group and to each panel and the user's drag positions survive reloads — sizes are written to localStorage under that key. Same drag, different visit, same layout.
<ResizablePanelGroup direction="horizontal" id="inbox-shell">
<ResizablePanel defaultSize={20} id="nav">…</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={30} id="list">…</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={50} id="detail">…</ResizablePanel>
</ResizablePanelGroup>Static or resizable — which one?
- Reach for AppShell
nav="three-pane"when the column widths are a design decision (320px aside, etc.) and you don't want users adjusting them. Pure CSS, server-renderable, no JS. - Reach for Resizable when the column widths are a workflow decision — the user's preferred split for their inbox, IDE layout, file browser. Drag handles + persistence outweigh the JS cost.
- They compose: a marketing page using AppShell and an in-app workspace inside it using Resizable is a perfectly valid shape.
ResizablePanelGroup props
| Prop | Type | Default | Description |
|---|---|---|---|
| direction | "horizontal" | "vertical" | — | Required. `horizontal` arranges panels left-to-right with vertical drag handles between them. `vertical` arranges panels top-to-bottom with horizontal handles. |
| id | string | — | When set, panel sizes are auto-persisted to localStorage under this key — the user's preferred layout survives reloads. Pair with an `id` on each `ResizablePanel`. |
| autoSaveId | string | — | Alternative to `id` for layout persistence. Both work; pick one. |
| onLayout | (sizes: number[]) => void | — | Fires whenever the user drags a handle and panels resize. Sizes are percentages summing to ~100. |
ResizablePanel props
| Prop | Type | Default | Description |
|---|---|---|---|
| defaultSize | number | — | Initial size as a percentage of the group (0–100). Required if you want a non-equal split on first render. |
| minSize | number | 10 | Minimum size before the panel collapses or hits the floor (percentage). |
| maxSize | number | 100 | Maximum size the panel can grow to (percentage). |
| collapsible | boolean | false | When true, dragging below `collapsedSize` snaps the panel closed. Pair with `onCollapse` / `onExpand`. |
| collapsedSize | number | 0 | Size the panel snaps to when collapsed (percentage). Set non-zero to keep an icon-rail visible when collapsed. |
| id | string | — | Stable id used by `ResizablePanelGroup`'s `id` to persist this panel's size across reloads. |
ResizableHandle props
| Prop | Type | Default | Description |
|---|---|---|---|
| withHandle | boolean | false | When true, render a small grip in the middle of the divider for affordance. Without it, the handle is a 1px hit area — fine for power-user tools, less discoverable for a general audience. |
| disabled | boolean | false | Disable dragging on this handle. |
When to use
- Inbox / Mail apps where users want to drag the message list wider or narrower.
- IDE-style layouts: editor + sidebar + console, where the developer's preferred ratio matters.
- Comparison views: split-pane diff, before/after editor, side-by- side translation.
- Anywhere the right answer to "how wide should this column be?" is "let the user decide."
Sidecar
The Markdown sidecar Studio (and the Grade MCP server, when it ships) reads to understand this component — frontmatter, when- to-use guidance, and canonical examples. Authored once at packages/ui/components/ui/resizable.md and shipped inside the published @gradeui/ui tarball.
---
name: Resizable
import: "@gradeui/ui"
subcomponents: [ResizablePanelGroup, ResizablePanel, ResizableHandle]
props:
- ResizablePanelGroup: direction: "horizontal" | "vertical" — required; sets the axis the user drags along
- ResizablePanelGroup: autoSaveId?: string — persists user-adjusted sizes to localStorage under this id
- ResizablePanelGroup: onLayout?: (sizes: number[]) => void
- ResizablePanel: defaultSize?: number — percent of group (0-100); siblings should sum to ~100
- ResizablePanel: minSize?, maxSize?: number — percent bounds
- ResizablePanel: collapsible?: boolean — allow this panel to collapse to zero
- ResizablePanel: collapsedSize?, onCollapse?, onExpand? — collapse behaviour controls
- ResizableHandle: withHandle?: boolean — show a visible drag affordance (default just a hit-zone)
when_to_use: A multi-pane layout where the user wants to drag the divider — Slack/Mail-style list+detail, IDE editor+terminal, side-by-side compare view. Static layouts shouldn't use this — reach for AppShell with nav="three-pane" (fixed widths) or Grid (responsive ladder). Built on react-resizable-panels under the hood.
composes_with: [AppShellMain (host the splitter inside main), ScrollArea (each panel's content), Card]
aliases: [resizable, splitter, split pane, drag divider, adjustable panels, resizer, split view, draggable divider, split pane resizer, ns split view]
---
```jsx
// List + detail with a draggable divider, saved between sessions.
<ResizablePanelGroup direction="horizontal" autoSaveId="inbox">
<ResizablePanel defaultSize={30} minSize={20}>
<InboxList />
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={70}>
<ConversationView />
</ResizablePanel>
</ResizablePanelGroup>
```