Resizable
Panels split by a handle a reader drags or moves with the arrow keys, horizontal or vertical.
Resizable splits a space into panels with a handle between each pair, so a reader decides
how much room a list gets against its detail. It wraps
react-resizable-panels v4 and styles the
handle as a hairline on bg-border, with an optional grip. The kit does not store the
layout, so a reader's choice resets on reload unless you save it (see Keep the layout).
Usage
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@/components/ui/resizable';
<ResizablePanelGroup orientation="horizontal">
<ResizablePanel defaultSize="60%">List</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize="40%">Detail</ResizablePanel>
</ResizablePanelGroup>The group fills its parent, so give the parent a height. Each handle must be a direct child of the group, between two panels.
Sizes and limits
A panel size is a number of pixels, or a string with a unit. defaultSize="60%" is 60
percent of the group, defaultSize={240} is 240 pixels, and a string with no unit counts as
a percent. minSize and maxSize take the same forms and stop the handle at those sizes, so
a pane cannot be dragged shut by accident. A panel with no defaultSize is sized
automatically from the room that is left.
A panel with collapsible collapses to its collapsedSize (0 by default) when it is dragged
below minSize. Enter on its handle collapses it or restores it to the size it had.
Double-click a handle to return its panels to their default sizes, or pass
disableDoubleClick to the handle to turn that off.
Horizontal or vertical
orientation on the group is "horizontal" by default and puts the panels side by side. Set
"vertical" to stack them, and the handle turns into a horizontal line with its grip turned
to match.
Keep the layout
The group does not persist anything. To remember a reader's layout, give the group an id,
read it back with the library's useDefaultLayout, and pass the result to the group.
import { useDefaultLayout } from 'react-resizable-panels';
const { defaultLayout, onLayoutChanged } = useDefaultLayout({ id: 'issues-split' });
<ResizablePanelGroup
id="issues-split"
orientation="horizontal"
defaultLayout={defaultLayout}
onLayoutChanged={onLayoutChanged}
>
{/* panels */}
</ResizablePanelGroup>It saves to localStorage unless you pass a storage with getItem and setItem, such as
one backed by a cookie. A layout restored in the browser can shift once on a page rendered
on the server, because the server only knows the default sizes.
Props
Resizable takes the props of the library's Group, Panel, and Separator, and adds one.
| Prop | Type | Default | What it does |
|---|---|---|---|
withHandle | boolean | false | On ResizableHandle. Draws a 12 by 16px grip on the line, with a Phosphor DotsSixVertical icon. |
The other props you will use are orientation, defaultLayout, onLayoutChanged, and
disabled on the group, defaultSize, minSize, maxSize, collapsible, and
collapsedSize on a panel, and disabled and disableDoubleClick on a handle.
Accessibility
- The handle is a
role="separator"element that is focusable (tabindex="0"), so a keyboard user can resize without a pointer. Without a handle between two panels there is nothing to focus. - On a focused handle the arrow keys along the group's axis move it 5 percent at a time (Left and Right in a horizontal group, Up and Down in a vertical one), Home and End move it as far as the limits allow, Enter collapses or restores a collapsible panel, and F6 moves focus to the next handle in the group, or the previous with Shift.
- The handle reports the size of the panel before it.
aria-valuenowis that panel's size in percent,aria-valueminandaria-valuemaxare the limits it can reach with the neighboring panels' limits applied, andaria-controlsnames the panel. Itsaria-orientationis the direction the handle moves in, so it isverticalin a horizontal group. - The handle is named "Resize". Pass
aria-label, such as "Resize list", so a screen reader says which divider it is on. - Focus shows a 2px ring on
:focus-visibleonly. In forced colors the handle keeps a transparent outline that the system paints, so focus stays visible. - The grip is a plain
<div>around anaria-hiddenicon, and the handle's name comes fromaria-label, not from the grip. - The line draws in
CanvasTextin forced colors, so it stays visible there. - The pointer target is wider than the 1px line. The library extends it to 10px for a mouse and 20px for touch, and the pseudo-element adds 4px, so a finger can grab the handle. The handle itself is not 44px.
- The kit adds no transition to resizing, so reduced motion changes nothing.
disabledon a handle or group removes it from the tab order and setsaria-disabled.
When to use something else
- Fixed columns, with a grid or flex, when the split should not change. A handle is a control a reader has to find, so use one only when the right size depends on the reader's content.
- Sheet when the detail should cover the page and be dismissed, like a filter or a settings form. Resizable keeps both panes on the page at once.
- Scroll area inside a panel when you want the styled scrollbar, because a panel scrolls its overflow with the browser's own bar.
- App shell for the frame that holds the sidebar and page, which has its own sizing.
Install
shadcn add @gesso/resizable