Resizable
StablePanels that people can resize by dragging or with the keyboard — for layouts whose proportions are personal.
Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/resizable.jsonUsage
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable"<div className="h-80">
<ResizablePanelGroup orientation="horizontal">
<ResizablePanel defaultSize="30%" minSize="20%">Sidebar</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize="70%">Content</ResizablePanel>
</ResizablePanelGroup>
</div>A group always fills its parent — size the parent, not the group. Sizes accept strings with units ("30%", "240px", "16rem"); a bare number means pixels, and a unitless string means percent.
direction is now orientation, PanelResizeHandle is Separator (wrapped as ResizableHandle), and defaultSize={30} means 30 pixels — write defaultSize="30%".Examples
Vertical
Stack panels with orientation="vertical" — a query above its results, an editor above a console.
select * from deployments where status = 'error'With a grip
withHandle draws a small grip on the divider. Use it when the divider is easy to miss — thin panels, low-contrast surfaces, touch devices.
Nested layout
Groups nest to build real application layouts. Here the explorer is collapsible with minSize and maxSize, so it snaps closed when dragged past its minimum; the terminal is collapsible too. Focus a divider and press Enter to collapse or restore the panel before it.
1import { cva } from "class-variance-authority"23export const buttonVariants = cva([4 "inline-flex items-center",5])
$ pnpm dev ✓ Ready in 594ms
Remembering the layout
useDefaultLayout restores a saved layout on mount and saves it after each resize. Give every panel an id so saved sizes map back to the right panel. localStorage exists only in the browser, so this preview renders the default layout on the server and switches to the saved one after hydration.
In a server-rendered Next.js app, store the layout in a cookie instead: the server can read it, so the first paint already has the saved sizes and nothing shifts.
import { cookies } from "next/headers"
import { InboxPanels } from "./inbox-panels"
export default async function InboxLayout() {
const saved = (await cookies()).get("layout:inbox")?.value
return <InboxPanels defaultLayout={saved ? JSON.parse(saved) : undefined} />
}"use client"
import type { Layout } from "react-resizable-panels"
export function InboxPanels({ defaultLayout }: { defaultLayout?: Layout }) {
return (
<ResizablePanelGroup
defaultLayout={defaultLayout}
onLayoutChanged={(layout) => {
document.cookie = `layout:inbox=${JSON.stringify(layout)}; path=/; max-age=31536000`
}}
>
<ResizablePanel id="list" defaultSize="40%" minSize="25%">…</ResizablePanel>
<ResizableHandle />
<ResizablePanel id="detail" defaultSize="60%">…</ResizablePanel>
</ResizablePanelGroup>
)
}Guidelines
When to use
- In tools where people spend long sessions and have strong preferences about space: editors, mail, file managers, dashboards with side panels.
- When two views compete for space and the right split depends on the task.
When not to use
- On marketing and content pages, where the layout should be composed for the reader.
- On small screens. Below the
mdbreakpoint, switch to a single column, tabs or a Sheet instead of divisible panels. - To show or hide a panel entirely — a toggle button is clearer than dragging to zero.
Set limits
Always give panels a minSize so content can't be crushed into uselessness, and a maxSize when one side must stay visible. Make a panel collapsible when hiding it completely is a legitimate choice.
Persist what people choose
A layout someone adjusted is a preference. Save it with useDefaultLayout (or your own storage via onLayoutChanged) so it survives reloads.
Accessibility
Each divider is a focusable role="separator" with aria-valuenow, aria-valuemin and aria-valuemax describing the size of the panel before it, and aria-orientation perpendicular to the group (a horizontal group has vertical dividers).
| Key | Behavior |
|---|---|
Tab | Moves focus to the next divider. The divider turns brand-colored while focused. |
←→ | Resizes a horizontal group by 5%. |
↑↓ | Resizes a vertical group by 5%. |
Home | Shrinks the panel before the divider to its minimum. |
End | Grows the panel before the divider to its maximum. |
Enter | Collapses the panel before the divider, or restores it — when that panel is collapsible. |
F6ShiftF6 | Moves focus to the next or previous divider in the same group. |
- Double-click a divider to reset its panels to their default sizes (disable with
disableDoubleClick). - Hit area. Dividers are 1px wide but their target is larger — the library enforces a minimum hit size, bigger for coarse pointers.
- Label the panels. Give panels headings or
aria-labels so a screen reader user knows what they're resizing.
API reference
ResizablePanelGroup
The container that lays out panels and dividers. Renders a <div> that fills its parent. Wraps Group from react-resizable-panels v4.
ResizablePanel
A resizable region. className applies to an inner <div>, so layout utilities won't fight the panel's flex sizing. Wraps Panel.
ResizableHandle
The divider between two panels. Wraps Separator.
The divider exposes its state as data-separator: inactive, hover, active (dragging), focus or disabled — prfct colors it with the brand scale on hover, drag and focus.