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.

Sidebar
Main

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.

Left
Right

Three-pane workspace

Slack/Mail/Notion shape with adjustable middle column. Each panel has minSize so users can't crush a pane to nothing.

Nav
List
Detail

Vertical split

Set direction="vertical" to stack panels with horizontal drag handles between them — useful for an editor + console layout.

Editor
Console

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.

Nav
List
Preview

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

PropTypeDefaultDescription
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.
idstringWhen 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`.
autoSaveIdstringAlternative to `id` for layout persistence. Both work; pick one.
onLayout(sizes: number[]) => voidFires whenever the user drags a handle and panels resize. Sizes are percentages summing to ~100.

ResizablePanel props

PropTypeDefaultDescription
defaultSizenumberInitial size as a percentage of the group (0–100). Required if you want a non-equal split on first render.
minSizenumber10Minimum size before the panel collapses or hits the floor (percentage).
maxSizenumber100Maximum size the panel can grow to (percentage).
collapsiblebooleanfalseWhen true, dragging below `collapsedSize` snaps the panel closed. Pair with `onCollapse` / `onExpand`.
collapsedSizenumber0Size the panel snaps to when collapsed (percentage). Set non-zero to keep an icon-rail visible when collapsed.
idstringStable id used by `ResizablePanelGroup`'s `id` to persist this panel's size across reloads.

ResizableHandle props

PropTypeDefaultDescription
withHandlebooleanfalseWhen 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.
disabledbooleanfalseDisable 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>
```