<!-- Protocore Design System — ResizablePanels -->
# ResizablePanels

- **Category:** Layout (`layout`)
- **Slug:** `layout/resizable-panels`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { ResizablePanels } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/layout/resizable-panels

> Two or more panels sharing an axis, split by hairline handles that drag with the pointer and resize with the arrow keys.

## How sizing works

Sizes are **percentages** that always conserve their pair's combined space: dragging a handle grows one neighbour and shrinks the other by the same amount, clamped so neither crosses its `minSize` / `maxSize`. Panels are laid out by flex grow-factor, so the split stays proportional as the container resizes. There is **no persistence** — `ResizablePanels` is a controlled primitive; wire `onSizesChange` to your own store if you want the layout to survive a reload.

## Props

### ResizablePanels.Root

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `className` | `string` | no | — | — |
| `defaultSizes` | `number[]` | no | — | Initial sizes in uncontrolled mode; falls back to each panel's `defaultSize`. |
| `direction` | `enum` | no | `horizontal` | Split axis. |
| `keyboardStep` | `number` | no | `10` | Percentage points an arrow-key press moves the focused handle. |
| `onSizesChange` | `((sizes: number[]) => void)` | no | — | Fires with the next sizes array whenever a handle moves. |
| `sizes` | `number[]` | no | — | Controlled sizes (percentages, one per panel). |
| `style` | `CSSProperties` | no | — | — |

### ResizablePanels.Panel

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `__index` | `number` | no | `0` | @internal Ordinal assigned by `Root`; do not set manually. |
| `className` | `string` | no | — | — |
| `defaultSize` | `number` | no | — | Initial size in percentage points. Missing panels share the remainder equally. |
| `maxSize` | `number` | no | `100` | Largest size this panel may grow to, in percentage points. |
| `minSize` | `number` | no | `0` | Smallest size this panel may shrink to, in percentage points. |
| `style` | `CSSProperties` | no | — | — |

### ResizablePanels.Handle

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `__index` | `number` | no | `0` | @internal Gap ordinal assigned by `Root`; do not set manually. |
| `aria-label` | `string` | no | `"Resize panels"` | Accessible name for the separator. |
| `className` | `string` | no | — | — |
| `style` | `CSSProperties` | no | — | — |

## Examples

### Horizontal split

Compose `ResizablePanels.Root` with `Panel`s and a `Handle` between each pair. Give panels a `defaultSize` (percentage) plus `minSize` / `maxSize` bounds; drag the hairline seam or focus it and press the arrow keys.

```tsx
import { ResizablePanels, Text } from "@protocore/pds";

export default function Demo() {
  return (
    <div
      style={{
        height: 260,
        border: "1px solid var(--pds-border-faint)",
        background: "var(--pds-color-surface)",
      }}
    >
      <ResizablePanels.Root aria-label="Explorer split">
        <ResizablePanels.Panel defaultSize={32} minSize={18} maxSize={50}>
          <div style={{ padding: "var(--pds-space-4)" }}>
            <Text mono size="sm" color="muted">
              SOURCES
            </Text>
            <Text mono size="sm">
              infra/vpc.tf
            </Text>
            <Text mono size="sm">
              infra/s3.tf
            </Text>
            <Text mono size="sm">
              services/api
            </Text>
          </div>
        </ResizablePanels.Panel>
        <ResizablePanels.Handle aria-label="Resize sources" />
        <ResizablePanels.Panel defaultSize={68} minSize={30}>
          <div style={{ padding: "var(--pds-space-4)" }}>
            <Text mono size="sm" color="muted">
              INSPECTOR
            </Text>
            <Text size="sm">
              Drag the hairline seam, or focus it with Tab and press the arrow keys to resize.
            </Text>
          </div>
        </ResizablePanels.Panel>
      </ResizablePanels.Root>
    </div>
  );
}
```

### Vertical split

Set `direction="vertical"` to stack panels top-to-bottom — the handle becomes a horizontal seam and Arrow Up / Down drive it.

```tsx
import { ResizablePanels, Text } from "@protocore/pds";

export default function Demo() {
  return (
    <div
      style={{
        height: 300,
        border: "1px solid var(--pds-border-faint)",
        background: "var(--pds-color-surface)",
      }}
    >
      <ResizablePanels.Root direction="vertical" aria-label="Query workspace">
        <ResizablePanels.Panel defaultSize={55} minSize={25}>
          <div style={{ padding: "var(--pds-space-4)" }}>
            <Text mono size="sm" color="muted">
              QUERY
            </Text>
            <Text mono size="sm">
              select count(*) from sessions
            </Text>
            <Text mono size="sm">
              where region = &apos;eu-central-1&apos;;
            </Text>
          </div>
        </ResizablePanels.Panel>
        <ResizablePanels.Handle aria-label="Resize query editor" />
        <ResizablePanels.Panel defaultSize={45} minSize={20}>
          <div style={{ padding: "var(--pds-space-4)" }}>
            <Text mono size="sm" color="muted">
              RESULTS
            </Text>
            <Text mono size="sm">
              count ...... 12,804
            </Text>
            <Text mono size="sm">
              took ....... 42ms
            </Text>
          </div>
        </ResizablePanels.Panel>
      </ResizablePanels.Root>
    </div>
  );
}
```

## Do & don't

**Do**

- Put a `Handle` between every adjacent pair of `Panel`s, in source order.
- Give each panel a sensible `minSize` so content never collapses to nothing.
- Pass an `aria-label` to each `Handle` describing what it resizes.
- Persist the layout yourself from `onSizesChange` when it should outlive the session.

**Don't**

- Don't nest interactive resize handles inside a scrolling region that traps the pointer.
- Don't rely on the component to remember sizes — it deliberately keeps no state across mounts.
- Don't omit the `Handle` and expect a seam; panels without a handle between them can't be resized.
- Don't set `minSize` + `maxSize` sums that leave a pair no room to move.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Tab` | Move focus to a resize handle. |
| `Arrow Left / Right` | Resize a horizontal split by one step (grow / shrink the leading panel). |
| `Arrow Up / Down` | Resize a vertical split by one step. |
| `Home` | Collapse the leading panel to its minimum. |
| `End` | Grow the leading panel to its maximum. |

**Notes**

- Each handle is a `role="separator"` with `aria-orientation` (perpendicular to the split axis), `aria-valuenow` / `aria-valuemin` / `aria-valuemax` reflecting the leading panel's size, and `aria-controls` pointing at that panel.
- The arrow-key step is configurable via `keyboardStep` (default 10 percentage points).
- The 1px seam widens to a 6px hit area and inks to the accent on hover, focus, and drag.

## Related

`sidebar`, `app-shell`, `panel`, `grid`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
