Components

Resizable

Accessible, draggable panel groups for building split layouts — sidebars, editors, and dashboards — that the user can resize horizontally or vertically.

Basic usage

Wrap panels in a ResizablePanelGroup with a fixed height, and drag the divider to resize.

Sidebar
Content

Vertical

Set direction="vertical" to stack panels and resize along the y-axis.

Header
Body

With handle

Pass withHandle to render a visible grip affordance that makes the divider easier to find and grab. The grip is a solid --control-edge chip — the same token as the divider — so it needs no extra outline to hold its own against the page.

One
Two

Nested groups

Nest a vertical group inside a horizontal panel to compose full IDE-style layouts.

Nav
Editor
Terminal

Remembering sizes

Give the group an autoSaveId and the layout survives a reload — the pattern behind the three-column list ▸ detail ▸ side-panel screens this component exists for. Combine it with collapsible so a panel can be tucked away to collapsedSize and come back the same width. Drag the divider below, reload the page, and it stays where you left it.

List
Detail

Props

ResizablePanelGroup

PropTypeDefaultDescription
directionrequired'horizontal' | 'vertical'Axis the panels are laid out and resized along.
autoSaveIdstring | nullPersists panel sizes to localStorage under this id across reloads.
storage{ getItem, setItem }localStorageCustom store backing autoSaveId — swap in a cookie or a server-synced store.
keyboardResizeBynumber | nullnullPercentage each arrow-key press moves the divider; null falls back to the library’s built-in 10. Shift+Arrow always jumps the full 100 regardless.
onLayout(sizes: number[]) => voidFires with the new percentage sizes whenever the layout changes.
classNamestringAdditional Tailwind classes — set a fixed height here.
...restPanelGroupPropsAll react-resizable-panels PanelGroup props are forwarded; the element carries data-slot="resizable-panel-group".

ResizablePanel

PropTypeDefaultDescription
defaultSizenumberInitial size as a percentage (0–100) of the group. Omitted panels split what is left evenly.
minSizenumber0Smallest size the panel can be dragged to, as a percentage.
maxSizenumber100Largest size the panel can be dragged to, as a percentage.
collapsiblebooleanfalseAllows the panel to snap past minSize down to collapsedSize.
collapsedSizenumber0Size the panel snaps to when collapsed.
onCollapse() => voidFires when the panel reaches its collapsed size.
onExpand() => voidFires when a collapsed panel opens again.
onResize(size: number, prevSize?: number) => voidFires with this panel’s new percentage size on every change.
ordernumberExplicit order when panels are conditionally rendered.
...restPanelPropsAll react-resizable-panels Panel props are forwarded; the element carries data-slot="resizable-panel".

ResizableHandle

PropTypeDefaultDescription
withHandlebooleanfalseRenders a visible grip in the centre of the divider.
disabledbooleanfalsePrevents the handle from being dragged.
hitAreaMargins{ coarse: number; fine: number }{ coarse: 15, fine: 5 }Pixels of slack around the 1 px divider that still register as a grab — wider for touch than for a mouse.
onDragging(isDragging: boolean) => voidFires on drag start and end — useful to suspend expensive renders while resizing.
tabIndexnumber0Tab order of the separator. The handle is keyboard-operable out of the box.
classNamestringMerged onto the divider, which is bg-control-edge and 1 px wide.
...restPanelResizeHandlePropsAll react-resizable-panels PanelResizeHandle props are forwarded; the element carries data-slot="resizable-handle".

Accessibility

  • Each handle is a focusable role="separator" exposing aria-valuenow, aria-valuemin, and aria-valuemax so assistive tech announces panel sizing.
  • Handles are keyboard-operable — focus with Tab and resize in keyboardResizeBy steps of 10% with the arrow keys; Shift + arrow, and Home / End, jump to the min and max sizes.
  • A focus-visible ring using the brand gold ring token appears on keyboard focus so the active divider is always visible.
  • The divider is filled with --control-edge, not the decorative --border step. It is a real control, so WCAG 1.4.11 asks 3:1 of it, and going through the role token is what lets one declaration lift every divider. Under data-contrast="high" control-edge measures 3.83:1 in light and 4.94:1 in dark. On the default palette it resolves to --input: the same #EDEEF1 step as border in light (1.16:1), but #434A5A in dark (2.00:1) where border sits at 1.10:1 — a known gap of that palette.
  • The visible grip rendered by withHandle is decorative; the divider itself is 1 px wide but hitAreaMargins extends the pointer target by 15 px on touch and 5 px with a mouse, so the target stays comfortable without a heavier line.