component
Resizable panes separated by draggable handles — the three-pane shell every IDE-shaped product needs.
Splitter is a compound: you place <SplitterPane> and <SplitterHandle> children yourself, interleaved in source order. That is deliberate rather than a panes-only API, because each pane holds arbitrary content and an explicit handle keeps the DOM order obvious.
Sizing is relative weights plus absolute pixel limits. size is a weight — three panes at 1 / 2 / 1 take a quarter, a half and a quarter of the space left after the handles — while min and max are pixels, because that is what content actually needs (“this sidebar stops being usable under 200px”). Because weights are the source of truth, resizing the container redistributes proportionally rather than replaying stale pixel values.
The default axis splits left/right. Drag a handle and only the two panes on either side of it move — panes further along never shift under the cursor, so a drag is a local negotiation rather than a relayout.
---
import { Splitter, SplitterPane, SplitterHandle } from "../../index";
import { css } from "@pindoba/styled-system/css";
const pane = css({ padding: "sm", fontSize: "sm" });
const frame = css({
width: "100%",
height: "12rem",
borderWidth: "1px",
borderStyle: "solid",
borderColor: "neutral.border.muted",
borderRadius: "sm",
overflow: "hidden",
});
---
<div class={frame}>
<Splitter
panes={[
{ id: "sidebar", size: 1, min: 70 },
{ id: "main", size: 3, min: 90 },
{ id: "inspector", size: 1, min: 70 },
]}
>
<SplitterPane id="sidebar"><div class={pane}>Sidebar</div></SplitterPane>
<SplitterHandle controls="sidebar" />
<SplitterPane id="main"><div class={pane}>Canvas</div></SplitterPane>
<SplitterHandle controls="main" />
<SplitterPane id="inspector"><div class={pane}>Inspector</div></SplitterPane
>
</Splitter>
</div>orientation="vertical" splits top/bottom. Set it on the root and on each handle, so a handle rendered on its own still knows which way it moves.
---
import { Splitter, SplitterPane, SplitterHandle } from "../../index";
import { css } from "@pindoba/styled-system/css";
const pane = css({ padding: "sm", fontSize: "sm" });
const frame = css({
width: "100%",
height: "14rem",
borderWidth: "1px",
borderStyle: "solid",
borderColor: "neutral.border.muted",
borderRadius: "sm",
overflow: "hidden",
});
---
<div class={frame}>
<Splitter
orientation="vertical"
panes={[
{ id: "editor", size: 2, min: 60 },
{ id: "output", size: 1, min: 48 },
]}
>
<SplitterPane id="editor"><div class={pane}>Editor</div></SplitterPane>
<SplitterHandle orientation="vertical" controls="editor" />
<SplitterPane id="output"><div class={pane}>Output</div></SplitterPane>
</Splitter>
</div>Mark a pane collapsible and it snaps shut once dragged past its min by collapseThreshold px — a deliberate push, not a twitch. Enter on an adjacent handle toggles the same state from the keyboard. A collapsed pane keeps its DOM (and its scroll position); only its track goes to zero.
---
import { Splitter, SplitterPane, SplitterHandle } from "../../index";
import { css } from "@pindoba/styled-system/css";
const pane = css({ padding: "sm", fontSize: "sm" });
const frame = css({
width: "100%",
height: "10rem",
borderWidth: "1px",
borderStyle: "solid",
borderColor: "neutral.border.muted",
borderRadius: "sm",
overflow: "hidden",
});
---
<div class={frame}>
<Splitter
panes={[
{ id: "nav", size: 1, min: 90, collapsible: true },
{ id: "body", size: 3, min: 90 },
]}
>
<SplitterPane id="nav"
><div class={pane}>
Drag me shut, or press Enter on the handle
</div></SplitterPane
>
<SplitterHandle controls="nav" />
<SplitterPane id="body"><div class={pane}>Content</div></SplitterPane>
</Splitter>
</div>Each handle is a role="separator" that is explicitly focusable — a bare separator is inert to assistive tech, so it is made operable on purpose — and carries a live aria-valuenow as a percentage of the split axis, plus aria-controls pointing at the pane it resizes.
| Key | Action |
|---|---|
| ← / → (horizontal) | Resize by keyboardStep px |
| ↑ / ↓ (vertical) | Resize by keyboardStep px |
| Home | Shrink the leading pane to its min |
| End | Grow it until the neighbour hits its min |
| Enter | Toggle a collapsible pane |
Only the keys along the splitter’s own axis do anything, so a vertical splitter never steals the horizontal arrows from its content. Note that aria-orientation describes the divider, which is perpendicular to the split: a left/right layout is divided by a vertical line.
Give the root a persistKey and the resolved weights are saved to localStorage and restored on the next mount. Weights (not pixels) are persisted, so a layout saved on a wide screen still makes sense on a narrow one. A corrupt or wrong-length stored value is ignored rather than allowed to break the layout.
The panes and handles, in source order — `<SplitterPane>` and `<SplitterHandle>` interleaved.
24Distance in px a pane must be dragged below its `min` before a `collapsible` pane snaps shut.
falseDisable every handle. Panes keep their current sizes.
Bound reference to the root element (`bind:this`).
"subtle"Weight of the divider line.
"neutral"Semantic palette the divider draws from, forwarded as `colorPalette`. The grip colour is published as an inheriting custom property, so this reaches every handle beneath the root without repeating it per handle.
16How far a keyboard arrow press resizes, in px.
"Resizable panes"Accessible label for the group of panes.
Fired when a pane collapses or expands.
Fired whenever the resolved pane geometry changes.
"horizontal"Axis the panes are laid out along. `horizontal` splits left/right, `vertical` splits top/bottom.
Pane sizing, in source order — one entry per `<SplitterPane>`. `size` is a relative weight; `min` / `max` are pixels.
Per-slot style and HTML-attribute overrides.
`localStorage` key under which the current sizes are saved and restored. Omit to keep the splitter stateless across reloads.
Sizes (in px, along the split axis) that a drag snaps to when it comes within `snapThreshold`.
8How close in px a drag must come to a `snap` stop before it latches.
Plus all standard <div> HTML
attributes.
Whether the pane is currently collapsed. Frameworks feed their reactive state in; the controller owns the transitions.
Identity of the pane, matching an entry in the root's `panes` array, and the target of the adjacent handle's `aria-controls`. Optional: the controller falls back to the pane's source position, so an unlabelled splitter still resizes — you only need an id to address a pane by name or to wire `aria-controls`.
Per-slot style and HTML-attribute overrides.
`id` of the pane immediately before this handle — the one the handle's ARIA value describes and that <kbd>Enter</kbd> collapses.
falseDisable this handle.
Whether a drag is in progress on this handle.
"Resize panes"Accessible label for the handle.
"horizontal"Axis the handle moves along. Normally inherited from the root; set it only when rendering a handle outside a `<Splitter>`.
Per-slot style and HTML-attribute overrides.
Largest percentage the controlled pane may reach.
Smallest percentage the controlled pane may reach.
Current size of the controlled pane as a percentage of the split axis, published as `aria-valuenow`. The controller keeps this live.