/// Layout
ResizablePanels
Two or more panels sharing an axis, split by hairline handles that drag with the pointer and resize with the arrow keys.
import { ResizablePanels } from "@protocore/pds";Horizontal split
Compose ResizablePanels.Root with Panels and a Handle between each pair. Give panels a defaultSize (percentage) plus minSize / maxSize bounds; drag the hairline seam or focus it and press the arrow keys.
SOURCES
infra/vpc.tf
infra/s3.tf
services/api
INSPECTOR
Drag the hairline seam, or focus it with Tab and press the arrow keys to resize.
Vertical split
Set direction="vertical" to stack panels top-to-bottom — the handle becomes a horizontal seam and Arrow Up / Down drive it.
QUERY
select count(*) from sessions
where region = 'eu-central-1';
RESULTS
count ...... 12,804
took ....... 42ms
Controlled sizes
Own the split by passing sizes and onSizesChange. Nothing is persisted for you — read the array out and store it wherever you like (URL, localStorage, a layout preset).
sizes
[50, 50]
Requests
Latency
How sizing works
Sizes are percentages that always conserve their pair's combined space: dragging a handle grows one neighbour and shrinks the other by the same amount, clamped so neither crosses its minSize / maxSize. Panels are laid out by flex grow-factor, so the split stays proportional as the container resizes. There is no persistence — ResizablePanels is a controlled primitive; wire onSizesChange to your own store if you want the layout to survive a reload.
Usage
Do
- Put a `Handle` between every adjacent pair of `Panel`s, in source order.
- Give each panel a sensible `minSize` so content never collapses to nothing.
- Pass an `aria-label` to each `Handle` describing what it resizes.
- Persist the layout yourself from `onSizesChange` when it should outlive the session.
Don't
- Don't nest interactive resize handles inside a scrolling region that traps the pointer.
- Don't rely on the component to remember sizes — it deliberately keeps no state across mounts.
- Don't omit the `Handle` and expect a seam; panels without a handle between them can't be resized.
- Don't set `minSize` + `maxSize` sums that leave a pair no room to move.
Accessibility
| Keys | Action |
|---|---|
| Tab | Move focus to a resize handle. |
| Arrow Left / Right | Resize a horizontal split by one step (grow / shrink the leading panel). |
| Arrow Up / Down | Resize a vertical split by one step. |
| Home | Collapse the leading panel to its minimum. |
| End | Grow the leading panel to its maximum. |
- Each handle is a `role="separator"` with `aria-orientation` (perpendicular to the split axis), `aria-valuenow` / `aria-valuemin` / `aria-valuemax` reflecting the leading panel's size, and `aria-controls` pointing at that panel.
- The arrow-key step is configurable via `keyboardStep` (default 10 percentage points).
- The 1px seam widens to a 6px hit area and inks to the accent on hover, focus, and drag.
ResizablePanels.Root props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | |
defaultSizes | number[] | — | Initial sizes in uncontrolled mode; falls back to each panel's `defaultSize`. |
direction | enum | horizontal | Split axis. |
keyboardStep | number | 10 | Percentage points an arrow-key press moves the focused handle. |
onSizesChange | ((sizes: number[]) => void) | — | Fires with the next sizes array whenever a handle moves. |
sizes | number[] | — | Controlled sizes (percentages, one per panel). |
style | CSSProperties | — |
ResizablePanels.Panel props
| Prop | Type | Default | Description |
|---|---|---|---|
__index | number | 0 | @internal Ordinal assigned by `Root`; do not set manually. |
className | string | — | |
defaultSize | number | — | Initial size in percentage points. Missing panels share the remainder equally. |
maxSize | number | 100 | Largest size this panel may grow to, in percentage points. |
minSize | number | 0 | Smallest size this panel may shrink to, in percentage points. |
style | CSSProperties | — |
ResizablePanels.Handle props
| Prop | Type | Default | Description |
|---|---|---|---|
__index | number | 0 | @internal Gap ordinal assigned by `Root`; do not set manually. |
aria-label | string | "Resize panels" | Accessible name for the separator. |
className | string | — | |
style | CSSProperties | — |