ComponentsLayout

Resizable

Panels that share a row or a column, split by a divider people can drag or move with the keyboard. Sizes can be limited and remembered between visits.

Orientations

Drag the divider, or focus it and use the arrow keys. Home and End jump to the first panel's limits.

Runs

  • Weekly digest
  • Invoice follow-up
  • Lead scoring

Run details

Weekly digest

Finished in 42 seconds. Sent to 18 people in the Growth workspace.

Side by side: 40% · 60%

Prompt

Summarise the open invoices older than 30 days.

Response

Four invoices are overdue, totalling $12,480. Northwind Ltd has the oldest, at 47 days.

Stacked: 40% · 60%

Chat and canvas

The canvas opens beside the chat with a grip. Sizes you choose are saved per panel set. Below 720px the canvas moves into a sheet.

Which invoices are overdue?

Three customers are more than 30 days late. I opened the details in the canvas.

Canvas is open.

Overdue invoices

CustomerDaysAmount
Northwind Ltd47$5,200
Bloom & Co38$3,180
Juniper Studio31$4,100

File explorer

A plain divider splits a file tree from the open file. The tree keeps at least 220px and the file at least 35%. Narrow screens show one pane at a time.

Accepted revision 9f3c2a71be04

Soul files

Select a file

Open a file from the tree to read its saved contents. Files cannot be edited here.

Usage

Resizable usageTSX
import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from "@tulipfarm/sprout/resizable";

<div className="h-64 rounded-lg border">
  <ResizablePanelGroup orientation="horizontal">
    <ResizablePanel defaultSize="28%" minSize="220px">
      <FileTree />
    </ResizablePanel>
    <ResizableHandle aria-label="Resize file tree" />
    <ResizablePanel minSize="35%">
      <OpenFile />
    </ResizablePanel>
  </ResizablePanelGroup>
</div>

The group fills its parent, so give the parent a height. orientation is "horizontal" (default), which places panels side by side, or "vertical", which stacks them. Panels and handles must be direct children of the group. Fragments are fine.

defaultSize, minSize, and maxSize take a percent such as "40%", or a length in px, rem, em, vw, or vh. A plain number is pixels. A string without a unit is a percent. Panels without a defaultSize share the space that is left. A px minimum is checked again when the group changes size.

Handle

ResizableHandle draws a 1px line with a 9px hit area (24px on touch screens). Add withHandle for a small grip on splits people resize often, like a chat and its canvas. Leave it off for quiet splits like a file tree. Always give the handle an aria-label that names what it resizes.

Saved layouts

Saved layout usageTSX
import { useResizableLayout } from "@tulipfarm/sprout/resizable";

const layout = useResizableLayout({
  id: "app:chat-canvas",
  panelIds: open ? ["chat", "canvas"] : ["chat"],
  onlySaveAfterUserInteractions: true,
});

<ResizablePanelGroup
  defaultLayout={layout.defaultLayout}
  onLayoutChanged={layout.onLayoutChanged}
>
  <ResizablePanel id="chat" minSize="40%" defaultSize={open ? "55%" : "100%"}>
    <Chat />
  </ResizablePanel>
  {open && (
    <>
      <ResizableHandle withHandle aria-label="Resize canvas" />
      <ResizablePanel id="canvas" minSize="320px" defaultSize="45%">
        <Canvas />
      </ResizablePanel>
    </>
  )}
</ResizablePanelGroup>

A layout is panel sizes in percent, keyed by panel id. defaultLayout is used when it names every panel shown. Otherwise each panel's defaultSize applies. When panels are added or removed, the layout starts again from the defaults or the saved layout for that set of panels.

onLayoutChange runs on every change, including each pointer move.onLayoutChanged runs once a change settles, with { isUserInteraction }. useResizableLayout saves to localStorage by default, or to any storage with getItem and setItem. With onlySaveAfterUserInteractions, it only saves sizes a person chose.

Accessibility

The handle is a focusable role="separator" with aria-valuenow, aria-valuemin, and aria-valuemax for the panel before it, and aria-controls pointing at that panel. Arrow keys along the split move it 5%. Home and End move it to that panel's minimum and maximum. Right-to-left pages flip the horizontal keys and drag.

Line and grip colors change in 120ms and stop when reduced motion is requested. While dragging, the page keeps a resize cursor and text is not selected.

Theme tokens

The line uses --sprout-line, --sprout-border on hover, and --sprout-focus while focused or dragging. The grip uses --sprout-border and --sprout-border-strong.