component

Splitter

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.

Horizontal

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.

Canvas
Inspector
---
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>

Vertical

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.

Editor
Output
---
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>

Collapsible

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.

Content
---
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>

Keyboard and Accessibility

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.

KeyAction
← / → (horizontal)Resize by keyboardStep px
↑ / ↓ (vertical)Resize by keyboardStep px
HomeShrink the leading pane to its min
EndGrow it until the neighbour hits its min
EnterToggle 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.

Persistence

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.

props · 28 shown · 28 total
Splitter
children slot svelte
Snippet

The panes and handles, in source order — `<SplitterPane>` and `<SplitterHandle>` interleaved.

collapseThreshold
number
default 24

Distance in px a pane must be dragged below its `min` before a `collapsible` pane snaps shut.

disabled
boolean
default false

Disable every handle. Panes keep their current sizes.

element binding svelte
HTMLDivElementnull

Bound reference to the root element (`bind:this`).

emphasis
"bold""subtle"
default "subtle"

Weight of the divider line.

feedback
"primary""neutral""success""warning""danger""inherit"
default "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.

keyboardStep
number
default 16

How far a keyboard arrow press resizes, in px.

label
string
default "Resizable panes"

Accessible label for the group of panes.

onCollapsedChange svelte
(id: string, collapsed: boolean) => void

Fired when a pane collapses or expands.

onSizesChange svelte
(panes: SplitterPaneState[]) => void

Fired whenever the resolved pane geometry changes.

orientation
"horizontal""vertical"
default "horizontal"

Axis the panes are laid out along. `horizontal` splits left/right, `vertical` splits top/bottom.

panes
SplitterPaneConfig[]

Pane sizing, in source order — one entry per `<SplitterPane>`. `size` is a relative weight; `min` / `max` are pixels.

passThrough
SplitterPassThrough<RootElementAttributes>

Per-slot style and HTML-attribute overrides.

persistKey
string

`localStorage` key under which the current sizes are saved and restored. Omit to keep the splitter stateless across reloads.

snap
number[]

Sizes (in px, along the split axis) that a drag snaps to when it comes within `snapThreshold`.

snapThreshold
number
default 8

How close in px a drag must come to a `snap` stop before it latches.

Plus all standard <div> HTML attributes.

SplitterPane
collapsed
boolean

Whether the pane is currently collapsed. Frameworks feed their reactive state in; the controller owns the transitions.

id
string

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`.

passThrough
SplitterPanePassThrough<PaneElementAttributes>

Per-slot style and HTML-attribute overrides.

SplitterHandle
controls
string

`id` of the pane immediately before this handle — the one the handle's ARIA value describes and that <kbd>Enter</kbd> collapses.

disabled
boolean
default false

Disable this handle.

dragging
boolean

Whether a drag is in progress on this handle.

label
string
default "Resize panes"

Accessible label for the handle.

orientation
"horizontal""vertical"
default "horizontal"

Axis the handle moves along. Normally inherited from the root; set it only when rendering a handle outside a `<Splitter>`.

passThrough
SplitterHandlePassThrough<HandleElementAttributes>

Per-slot style and HTML-attribute overrides.

valueMax
number

Largest percentage the controlled pane may reach.

valueMin
number

Smallest percentage the controlled pane may reach.

valueNow
number

Current size of the controlled pane as a percentage of the split axis, published as `aria-valuenow`. The controller keeps this live.

Type

  • Components
  • Blocks