Resizable
Split-panel primitive (react-resizable-panels) for user-resizable layouts. A ResizablePanelGroup lays panels horizontally or vertically; a ResizableHandle between them exposes a 4px hit area with an optional 12×16 grip (Yuno tokens: bg-border + border-border + rounded-xs, GripVertical light size-2.5). Direction, min/max sizes, and collapsible behavior are per-panel props.
Anatomy
Three parts. The Group is the flex container that owns the orientation; Panels are the flex children with size constraints; the Handle is the interactive divider between them.
- 1ResizablePanelGroup
The flex container. orientation='horizontal' → row (default); orientation='vertical' → column. w-full h-full by default — you set the outer bounds via className (rounded-lg border, fixed height, max-width). Persist layout across reloads with autoSaveId.
- 2ResizablePanel
A flex child that responds to drag. Set defaultSize (0-100, %), minSize / maxSize (%) to guard against disappearing content, and optionally collapsible + collapsedSize for panels that can snap closed.
- 3ResizableHandle
The interactive divider. Ships a 4px hit area (visible line is 1px) between the two panels. Focusable, keyboard-resizable via arrow keys. withHandle renders the small centered grip so users can see the handle.
- 4Grip (inside Handle when withHandle)
12px × 16px pill, rounded-xs (2px), bg-border with border-border, containing a 10px Phosphor DotsSixVertical (weight='light'). Rotates 90° when orientation='vertical'.
Variants
Two axis (orientation) × two handle styles = four presets. Orientation is set on the Group; handle style is set per Handle via the withHandle prop.
<ResizablePanelGroup orientation="horizontal" className="rounded-lg border">
<ResizablePanel defaultSize={40}>Left</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={60}>Right</ResizablePanel>
</ResizablePanelGroup><ResizablePanelGroup orientation="vertical" className="rounded-lg border">
<ResizablePanel defaultSize={40}>Top</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={60}>Bottom</ResizablePanel>
</ResizablePanelGroup><ResizablePanelGroup orientation="horizontal" className="rounded-lg border">
<ResizablePanel defaultSize={50}>Left</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={50}>Right</ResizablePanel>
</ResizablePanelGroup>States
Handle states: default (1px bg-border), hover (4px hit area picks up the pointer), focus (ring-1 ring-ring ring-offset-1 — keyboard resize with arrow keys), dragging (cursor col-resize / row-resize, panels resize live).
Recipes
Ready-to-copy compositions covering the most common Yuno usages of this atom.
Horizontal 2-panel split with a grip handle. defaultSize 40/60 with minSize 25 on both so neither panel can disappear. The pattern for any 'code left, preview right' or 'form left, live result right' prototype.
<ResizablePanelGroup orientation="horizontal" className="rounded-lg border">
<ResizablePanel defaultSize={40} minSize={25}>
{/* Editor */}
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={60} minSize={25}>
{/* Preview */}
</ResizablePanel>
</ResizablePanelGroup>Common editor layout: skinny sidebar, wide main, skinny inspector. Set minSize 15/40/15 so the middle panel keeps priority. Two handles, both with grip.
<ResizablePanelGroup orientation="horizontal" className="rounded-lg border">
<ResizablePanel defaultSize={20} minSize={15}>
{/* Sidebar */}
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={60} minSize={40}>
{/* Main */}
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={20} minSize={15}>
{/* Inspector */}
</ResizablePanel>
</ResizablePanelGroup>Vertical direction: chat on top, terminal below. Grip rotates 90° automatically. Use when the two regions relate to the same object but need independent height.
<ResizablePanelGroup orientation="vertical" className="rounded-lg border">
<ResizablePanel defaultSize={60} minSize={30}>
{/* Chat */}
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={40} minSize={20}>
{/* Terminal */}
</ResizablePanel>
</ResizablePanelGroup>First panel with collapsible + collapsedSize=0 snaps closed when dragged past the threshold. Wire onCollapse / onExpand to toggle icons or persist state. Skip withHandle here — the collapse gesture is the primary interaction.
const [collapsed, setCollapsed] = React.useState(false);
<ResizablePanelGroup orientation="horizontal" autoSaveId="workspace">
<ResizablePanel
defaultSize={20}
minSize={12}
collapsible
collapsedSize={0}
onCollapse={() => setCollapsed(true)}
onExpand={() => setCollapsed(false)}
>
{/* Sidebar — hides when collapsed */}
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={80}>
{/* Main */}
</ResizablePanel>
</ResizablePanelGroup>Import
Single import from @/components/ui/resizable — Group, Panel and Handle in one line.
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";Props
The most-used props across the three exports. See react-resizable-panels docs for the full API.
| Prop | Type | Default | Description |
|---|---|---|---|
| PanelGroup.orientation | "horizontal" | "vertical" | "horizontal" | Layout axis. Horizontal = row (default); vertical = column. The grip icon rotates 90° when vertical. |
| PanelGroup.autoSaveId | string | — | Persist the current layout to localStorage under this key so it survives reloads. Use a per-prototype key like 'workspace' or 'chat-app'. |
| PanelGroup.onLayoutChange | (sizes: number[]) => void | — | Fires whenever the layout changes. Sizes are percentages that sum to 100. |
| Panel.defaultSize | number | — | Initial size (0-100, percent). Should sum to 100 across all panels in the same Group. |
| Panel.minSize | number | 10 | Lower bound in percent. Guard against content disappearing when the user drags aggressively. |
| Panel.maxSize | number | 100 | Upper bound in percent. Rarely needed unless one panel must never dominate. |
| Panel.collapsible | boolean | false | Allow the user to snap the panel closed by dragging past a threshold. Pair with collapsedSize (usually 0). |
| Panel.collapsedSize | number | 0 | The size (percent) the panel snaps to when collapsed. |
| Panel.onCollapse / onExpand | () => void | — | Fires when the panel enters / leaves the collapsed state. Wire to icon swaps or persisted state. |
| Handle.withHandle | boolean | false | When true, renders the 12×16 rounded-xs grip centered on the divider (Yuno tokens). Turn on for primary splits; leave off for tertiary dividers where the interaction is secondary. |
| Handle.disabled | boolean | false | Locks the handle — panels stop responding to drag / arrow keys. Use during read-only states. |
| Handle.className | string | — | Merged onto the divider. Do not override the grip radius / bg / border — those are Yuno tokens. |
When to use
- Editor prototypes (code / preview, form / result).
- Multi-region workspaces where the user should tune column widths (three-panel apps).
- Any panel the user should be able to resize or collapse.
When not to use
- Mobile screens — resizing panels does not map to touch.
- The Dashboard chrome (Sidebar + TopBar) — that shell is fixed, not user-resizable.
- Layouts where the size ratio is a product decision, not a user preference — use a plain flex layout with static widths.
Usage
- Set minSize / maxSize on every Panel so no panel can disappear or dominate.
- Show the grip (withHandle) whenever the split is a primary interaction — it's the discoverability affordance.
- Use autoSaveId on the Group to persist the layout across reloads.
- Wrap the whole Group in a rounded-lg border so the outer bounds read as a single surface.
- Don't nest more than 2 levels deep — it stops being clear which handle the user is dragging.
- Don't hide the handle in a primary split — users need to see something is draggable.
- Don't override the grip radius, bg or icon — that's the Yuno token spec.
- Don't use it on mobile prototypes — swap for a Sheet.
Related
Cross-links to atoms and patterns you may reach for next.
- Scroll areaWrap panel content in ScrollArea so it doesn't clip when the panel gets narrow.
- SheetFor mobile — Resizable does not translate to touch; use a Sheet or Drawer instead.
- Sidebar (organism)The Dashboard sidebar is a fixed shell, not a Resizable — don't compose them.
- CardWrap the whole Group in a Card when it needs surface elevation.