Gesso
Layout

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).

List
Detail

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.

Messages
Composer

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.

PropTypeDefaultWhat it does
withHandlebooleanfalseOn 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-valuenow is that panel's size in percent, aria-valuemin and aria-valuemax are the limits it can reach with the neighboring panels' limits applied, and aria-controls names the panel. Its aria-orientation is the direction the handle moves in, so it is vertical in 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-visible only. In forced colors the handle keeps a transparent outline that the system paints, so focus stays visible.
  • The grip is a plain <div> around an aria-hidden icon, and the handle's name comes from aria-label, not from the grip.
  • The line draws in CanvasText in 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.
  • disabled on a handle or group removes it from the tab order and sets aria-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

On this page