Components

ResizablePanel

An adjustable-panel primitive — drag to resize, dock or float.

The primitive behind every draggable edge here (the sidebar, the record-details pane, an AI chat pane). Two elements carry the layout: an invisible spacer that reserves flow width, and the panel itself, drawn on top of it. The spacer alone animates width; the panel animates transform — that split is what lets the panel move between docked (in-flow, a hairline divider, no elevation) and floating (position: fixed, an 8px gutter, r10, the measured floating elevation) without a reflow glitch on the rest of the layout.

Scoping caveat. The recipe emits one nested block rooted at .resizablepanel, so the handle rules only match inside that root. Using the column handle on its own — a table header, say — means putting className="resizablepanel" on the container that holds the column edges. Worth knowing before wiring it into a grid.

Constraints (min/max) are CSS min-width/max-width on the panel, never re-applied in JS — drag math writes an unclamped width and the rendered width is read back after layout, so a raw CSS max (the real min(60%, 100% - 350px)) clamps correctly without duplicating the formula in a resize handler. Width persists as a percentage of the container (matching the measured localStorage values), so the panel keeps its proportion when the window resizes.

Available since@crisp-ui-kit/crisp@0.11.0

Installation

npm install @crisp-ui-kit/crisp

Props

Prop

Type

Usage

import { ResizablePanel } from "@crisp-ui-kit/crisp";

<ResizablePanel
  defaultSize={280}
  min={200}
  max="min(60%, 100% - 350px)"
  storageKey="my-app:sidebar-width"
>
  <nav>…</nav>
</ResizablePanel>

Float it — the panel detaches from flow into a position: fixed overlay (the record-preview-peek pattern):

<ResizablePanel floating={isNarrow} defaultSize={280}>
  <nav>…</nav>
</ResizablePanel>

data-dragging is only set on the handle after the pointer moves — a click on the handle is never mistaken for a drag.

Double-clicking the handle resets the panel to defaultSize (#795, matching Attio's own record-page divider, confirmed live: dragged to a wider width, double-clicked, snapped back to its original default exactly). Controlled panels (size given) fire onSizeChange(defaultSize) instead of resetting internal state directly — the same split every other commit in this component already makes. Goes through the same storageKey persistence a drag does, so a reset is remembered across reload too, not just visually undone.

Examples

Handle variants

handleVariant="panel" (default) is a 10px hit target whose only visible mark is a 1px line (::before) that thickens to 2px and turns brand-solid on hover/drag, sitting flush inside the panel's inner edge.

handleVariant="column" is the second, genuinely different geometry for a table's column edges — a 7px hit target straddling a 1px divider, with a real 3px inner bar whose opacity comes in on a 100ms delay (so a quick pass across a header doesn't flash every edge):

On this page