NimUI
ComponentsLayout

Resizable

Draggable two-pane split with a keyboard-accessible handle

The Resizable component splits a region into two panes with a divider the operator can drag. It exists for the layout backoffices reach for constantly — a record list beside a detail panel — where the right ratio depends on the screen, the dataset, and the person reviewing it.

Grid and Flex distribute space by a rule you decide at build time; Resizable hands that ratio to the user and reports it back through onSizeChange, so it can be persisted per operator. It is dependency-free: pointer drag plus a full keyboard model on a role="separator" handle.

Import

import { Resizable } from '@nim-ui/components';

Playground

Drag the divider, or focus it and use the arrow keys. The root needs an explicit height.

Editable
<Resizable className="h-64" defaultSize={38} minSize={25} maxSize={70} handleLabel="Resize order list">
<div className="flex h-full flex-col gap-2 overflow-auto p-3 text-sm">
  <div className="font-medium text-neutral-900 dark:text-neutral-100">Orders</div>
  <Dot status="pending">ord_9021</Dot>
  <Dot status="processing" pulse>ord_9020</Dot>
  <Dot status="success">ord_9019</Dot>
  <Dot status="failed">ord_9018</Dot>
</div>
<div className="flex h-full flex-col gap-2 overflow-auto p-4 text-sm">
  <div className="text-base font-semibold tracking-tight text-neutral-900 dark:text-neutral-100">ord_9020</div>
  <div className="text-neutral-600 dark:text-neutral-400">Label printing — carrier handoff pending.</div>
  <div className="tabular-nums text-neutral-600 dark:text-neutral-400">Total 1,284.00 THB</div>
</div>
</Resizable>

List beside an inspector

The motivating case. Give the first pane the list, the second the detail panel, and size the root — panes inherit the height.

Record List and Inspector
ord_9020 — Bangkokord_9019 — Chiang Maiord_9018 — Phuketord_9017 — Khon Kaenord_9016 — Hat Yaiord_9015 — Rayong
ord_9020
Processing
Customer
Napat S.
Total
1,284.00 THB
Carrier
Kerry
Code
<Resizable className="h-72" defaultSize={38} minSize={25} maxSize={65} handleLabel="Resize order list">
  <ScrollArea className="h-full w-full">{/* order list */}</ScrollArea>
  <RecordInspector aria-label="Order inspector">{/* detail panel */}</RecordInspector>
</Resizable>

Vertical split

direction="vertical" stacks the panes and turns the handle into a horizontal bar — results above, a log console below.

Vertical Split
Query results
1,482 rows · 34 ms
payments.settled — 1,204
payments.refunded — 278
12:04:18 worker acquired batch 4821
12:04:19 settled 1204 payments
12:04:21 retry queue drained
Code
<Resizable direction="vertical" className="h-72" defaultSize={60} minSize={25} maxSize={80}>
  <div className="h-full overflow-auto p-4">{/* results */}</div>
  <div className="h-full overflow-auto p-4 font-mono text-xs">{/* log console */}</div>
</Resizable>

Controlled size

Pass size and onSizeChange when the ratio belongs to your state — persisting it per operator, or syncing two splits. onSizeChange fires in uncontrolled mode too, so you can save the ratio without owning it.

function OrderWorkspace() {
  const [size, setSize] = useState(() => loadPreference('orders.split') ?? 38);

  return (
    <Resizable
      className="h-[calc(100vh-8rem)]"
      size={size}
      onSizeChange={(next) => {
        setSize(next);
        savePreference('orders.split', next);
      }}
      minSize={25}
      maxSize={65}
      handleLabel="Resize order list"
    >
      <OrderList />
      <RecordInspector aria-label="Order inspector">{/* … */}</RecordInspector>
    </Resizable>
  );
}

Locked split

disabled freezes the ratio while keeping the handle in the tab order, so the layout stays legible during read-only or loading states.

Disabled Handle
Locked list
Locked detail
Code
<Resizable className="h-40" defaultSize={50} disabled>
  <div className="h-full p-4">Locked list</div>
  <div className="h-full p-4">Locked detail</div>
</Resizable>

Props

NameTypeDefaultDescription
childrenReactNode-Exactly two panes; extras are ignored and fewer render without a handle
direction'horizontal' | 'vertical''horizontal'Split axis — horizontal places panes side by side with a vertical handle
defaultSizenumber50Uncontrolled starting size of the first pane, as a percentage
sizenumber-Controlled size of the first pane, as a percentage
onSizeChange(size: number) => void-Called with the new first-pane percentage on every resize
minSizenumber15Smallest allowed first-pane percentage
maxSizenumber85Largest allowed first-pane percentage
handleLabelstring'Resize panes'Accessible name for the drag handle
disabledbooleanfalseLock the split — the handle stays focusable but ignores input
classNamestring-Size the split here (h-*, max-h-*) — panes inherit it

Accessibility

  • The handle is a role="separator" with tabIndex=0, aria-orientation, and aria-valuenow / aria-valuemin / aria-valuemax reporting the first pane's percentage. aria-valuetext reads it as a percentage, and aria-controls points at the pane the handle sizes.
  • minSize / maxSize are percentages: they are clamped into 0–100 and normalized if passed reversed, so the reported range is always valid.
  • Keyboard: Arrow Left / Arrow Right (Arrow Up / Arrow Down when vertical) nudge by 1%, Shift + arrow by 10%, Home jumps to minSize, End to maxSize, and Enter resets to defaultSize.
  • Always set handleLabel when a page has more than one split, so each separator has a distinct name.
  • disabled sets aria-disabled rather than removing the handle from the tab order — focus order stays stable when the lock lifts.
  • Focus shows the standard steel ring; the handle never relies on hover alone to signal that it is interactive.

Best Practices

Do

  • Give the root an explicit height (h-* or max-h-*) — panes size themselves from it
  • Set minSize / maxSize so neither pane can be dragged into uselessness
  • Persist the controlled size per operator; the ratio is a personal preference
  • Put a ScrollArea inside a pane when its content can overflow

Don't

  • Nest Resizables to build a three-pane grid — reach for Grid or a dedicated layout
  • Pass more than two children and expect a third pane; extras are ignored
  • Use it for content that only needs a fixed ratio — Grid or Flex says that more clearly
  • Rely on drag alone in a keyboard-first backoffice; the arrow-key model is the primary path
  • RecordInspector - The detail panel this layout usually holds
  • ScrollArea - Bounded scrolling inside a pane
  • Grid - Fixed-ratio layout when the user should not adjust it
  • AdminShell - The outer backoffice frame

On this page