Resizable
Resizable panel groups with draggable, keyboard-friendly handles, collapsible panels and layouts that survive a reload.
Preview
Usage
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@vitnode/core/components/ui/resizable'<div className="h-80">
<ResizablePanelGroup className="rounded-lg border">
<ResizablePanel defaultSize="35%">Sidebar</ResizablePanel>
<ResizableHandle />
<ResizablePanel>Content</ResizablePanel>
</ResizablePanelGroup>
</div>The group sets an inline height: 100%, so a height class on it is ignored.
Give the parent a height instead.
Groups nest: put a ResizablePanelGroup inside a ResizablePanel to split it
again, like the terminal in the preview.
Vertical
<ResizablePanelGroup orientation="vertical">With handle
withHandle adds a grip, so people know the divider can be grabbed.
<ResizableHandle withHandle />Sizes
The value's type decides the unit:
- a string without a unit is a percentage:
"30"equals"30%"; - a number is pixels:
minSize={180}is 180px, not 180%; - other CSS units work too:
"12rem","20vw".
<ResizablePanel defaultSize="25%" maxSize="40%" minSize={180}>Panels without defaultSize share the leftover space. Double-click a handle to
reset its panel to defaultSize.
Collapsible
A collapsible panel snaps to collapsedSize once dragged below minSize.
For a toggle button, call collapse() or expand() on its panelRef.
<ResizablePanel collapsedSize="8%" collapsible defaultSize="35%" minSize="25%">Persisting the layout
defaultLayout takes a map of panel id to percentage. onLayoutChanged
fires after every finished resize. Wire both to storage, and give each panel a
stable id.
const storageKey = 'my-layout'
const readLayout = () => {
try {
return localStorage.getItem(storageKey)
} catch {
return null
}
}
export const RememberedLayout = () => {
const stored = React.useSyncExternalStore(
() => () => {},
readLayout,
() => null,
)
return (
<ResizablePanelGroup
defaultLayout={stored ? JSON.parse(stored) : undefined}
onLayoutChanged={(layout, meta) => {
if (!meta.isUserInteraction) return
localStorage.setItem(
storageKey,
JSON.stringify(meta.requestedLayout ?? layout),
)
}}
>
<ResizablePanel defaultSize="40%" id="sidebar">
Sidebar
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel id="content">Content</ResizablePanel>
</ResizablePanelGroup>
)
}The null server snapshot keeps SSR happy: the server renders default sizes,
then the browser swaps in the saved ones. Save meta.requestedLayout when it
exists. It's what the user asked for, before a small screen squeezed it.
Accessibility
- Each handle is a focusable
role="separator"witharia-valuenow,aria-valuemin,aria-valuemaxandaria-controls. - Arrow keys resize by 5% along the group's orientation.
HomeandEndjump to the minimum or maximum size.Entercollapses or expands acollapsiblepanel.F6moves to the next handle,Shift + F6to the previous one.- Touch dragging only claims the resize axis, so panels still scroll the other way.
Props
ResizablePanelGroup
Prop
Type
ResizablePanel
Prop
Type
ResizableHandle
Prop
Type