Resizable
Let people resize or collapse panels with a pointer or keyboard, and save the layout.
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
It's shadcn's resizable on react-resizable-panels 4, which installs with it and owns the behavior: pointer and touch drags, limits, collapsing and the keyboard separator.
Use resizable panels for workspaces where people decide how much room each part gets: an editor and its terminal, a mail list and its reading pane, a board and a detail panel. Drag the sidebar past its minimum and it snaps shut to an icon rail. For a sidebar that opens and closes but never resizes, use the Sidebar. For content that overflows a fixed box, use a Scroll Area.
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@youj/react'
export function Mail() {
return (
<ResizablePanelGroup orientation="horizontal">
<ResizablePanel id="list" defaultSize="32%" minSize="16rem">
<MessageList />
</ResizablePanel>
<ResizableHandle aria-label="Resize message list" />
<ResizablePanel id="thread" minSize="40%">
<Thread />
</ResizablePanel>
</ResizablePanelGroup>
)
}The group fills its parent, so give the parent a height. Sizes take pixels as numbers, or strings in %, px or rem. Groups nest: put a vertical group inside a panel of a horizontal one, as the demo's editor and terminal do.
Description
An editor workspace: a sidebar that snaps shut to an icon rail when dragged past its minimum, beside a code panel and a terminal.
A collapsible panel snaps shut when it's dragged below its minSize, to collapsedSize (zero by default), and snaps open again when dragged back. Enter on its separator collapses or restores it. onCollapsedChange tells you when it changes, and the panel carries data-collapsed while shut, so what's inside can switch to a compact layout, like the demo's icon rail.
A vertical group with a grip on its separator.
orientation="vertical" stacks the panels. withHandle adds a grip to the separator, for layouts where the line alone doesn't read as something to drag. Its dots fuse into bars while it's held.
A layout that is saved and comes back after a reload.
onLayoutChanged fires once a drag or key press settles, not on every pointer move, so it's safe to save from. Pass the saved layout back as defaultLayout. Resize the panels, reload the page and they come back as you left them.
Read storage after hydration, as the example does, so the server and the first client render agree.
Code
resizable-demo.tsx, the Demo story. The component itself comes from the package.
Reference
PropsonCollapsedChange, withHandle (2)
Props
| Prop | Type | |
|---|---|---|
onCollapsedChange | (collapsed: boolean) => voidFires when a collapsible panel collapses or re-opens (by drag, keyboard or the imperative API). | – |
Other props pass through: extends ResizablePrimitive.PanelProps. | ||
ResizableHandle
| Prop | Type | |
|---|---|---|
withHandle | booleanShow a grip on the line. Use it where the separator isn't otherwise obvious. | – |
Other props pass through: extends ResizablePrimitive.SeparatorProps. | ||
ResizablePanelGroup takes react-resizable-panels' Group props: orientation, defaultLayout, onLayoutChange, onLayoutChanged and groupRef. ResizablePanel also takes the library's Panel props, such as defaultSize, minSize, maxSize, collapsible, collapsedSize and panelRef, whose handle (ResizablePanelHandle) can collapse(), expand() and resize() from code.
MotionKeyboard and imperative resize, pointer drag, separator wakes… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
Keyboard and imperative resizethe panels glide (flex-grow), including Enter to collapse or restore | duration.baseease.standard220ms · cubic-bezier(0.2, 0, 0, 1) | 220mscubic-bezier(0.2, 0, 0, 1) | |
| Pointer dragfollows the pointer exactly, snap included | no token pointer-driven | pointer-driven | |
| Separator wakesthe 1px line grows a 3px rail after a beat of hover, so crossing it doesn't flash | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) | |
| Dragged or focusedthe rail turns the ring color at once | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) | |
| Gripscales to 1.1 while held, and its dots fuse into bars (GripIcon) | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) |
Reduced motion
Every resize is instant, and the grip doesn't scale while held.
Reduced motion
Keyboard, Enter and imperative resizes jump to their new sizes, and the grip holds its size while held. Dragging is unchanged, because it follows the pointer either way.
AccessibilityKeyboard, ARIA, focus
Accessibility
- Each separator is a focusable
role="separator"witharia-valuenow,aria-valueminandaria-valuemax, and an orientation. Name it witharia-label("Resize sidebar") when its purpose isn't obvious from the layout. - Arrow keys resize in 5% steps, Home and End go to the limits, and Enter collapses or restores a collapsible panel. F6 moves to the next separator.
- A double-click on the separator resets the panels to their default sizes.
- The visible line is 1px, but the hit area is 27px wide, and 37px on touch screens.
- Focus shows as the accent rail, the same as a drag.