Splitter
A splitter divides a space into panels the reader can resize. It fills whatever contains it, so the container is what decides the overall size — a splitter with nothing around it has nothing to divide.
import { Splitter, SplitterHandle, SplitterPanel } from "ropav";<Splitter
aria-label="Editor layout"
class="h-48 w-full max-w-sm overflow-hidden rounded-xl border border-[var(--rp-border)]"
>
<SplitterPanel class="grid h-full place-items-center text-sm" default-size="1fr" min-size="80px">
Sidebar
</SplitterPanel>
<SplitterHandle />
<SplitterPanel class="grid h-full place-items-center text-sm" default-size="2fr" min-size="80px">
Editor
</SplitterPanel>
</Splitter>Panels and handles alternate, and there are as many handles as there are gaps. A horizontal splitter puts its panels side by side, which makes each handle a vertical line.
Sizing
default-size takes fr, px or %. A px panel holds its width when the container resizes and the fr beside it absorbs the difference, which is the shape of most sidebars. min-size and max-size clamp a drag and cannot be given in fr — a fraction has no fixed size to compare against.
is-collapsible changes what happens at the minimum: instead of stopping there, dragging past it snaps the panel shut. collapsed-size is what shut means — 0 takes the panel out of the layout, 48 leaves an icon rail.
splitter-collapsible.vue
<script setup lang="ts">
import { Splitter, SplitterHandle, SplitterPanel } from "ropav";
</script>
<template>
<Splitter aria-label="Collapsible sidebar" class="frame">
<SplitterPanel
class="pane"
:collapsed-size="48"
default-size="200px"
is-collapsible
min-size="160px"
>
Drag me shut
</SplitterPanel>
<SplitterHandle show-grip />
<SplitterPanel class="pane">Editor</SplitterPanel>
</Splitter>
</template>
<style scoped>
.frame {
border-color: var(--rp-border);
height: calc(var(--rp-spacing) * 56);
width: 100%;
max-width: var(--rp-container-lg);
overflow: hidden;
border-radius: calc(var(--rp-radius) * 1.5);
border-style: solid;
border-width: 1px;
}
.pane {
display: grid;
height: 100%;
place-items: center;
font-size: var(--rp-text-sm);
line-height: var(--rp-text-sm--line-height);
}
</style>show-grip draws the mark across the middle of the divider. It is decoration: the grab area reaches well past the visible line either way, so turning it off makes the handle no harder to hit.
Remembering the layout
auto-save-id stores the sizes under ropav:splitter:<id> in localStorage and reads them back once after mount. A stored layout is discarded unless it still matches the panels on screen, which is why panels worth remembering should carry explicit ids — generated keys stop matching the moment a panel is added, removed or reordered.
<Splitter aria-label="Workspace" auto-save-id="workspace">
<SplitterPanel id="sidebar" default-size="240px" min-size="160px">…</SplitterPanel>
<SplitterHandle id="sidebar-handle" />
<SplitterPanel id="editor">…</SplitterPanel>
</Splitter>Controlled sizes
Pass sizes and a drag reports what it would do rather than doing it, leaving the layout to whatever you write back. default-sizes is the uncontrolled equivalent, and a panel can take size or default-size of its own instead.
Accessibility
- Each handle is a
role="separator"with the ARIA value attributes for the panel before it, so a reader hears a size rather than a divider. - Its
aria-orientationis the inverse of the layout: a horizontal splitter has vertical dividers, and the attribute describes the divider. - Arrow keys along the axis move the handle by
keyboard-step, and with Shift bykeyboard-large-step. Home and End push it as far as it will go, Enter collapses or restores a collapsible neighbour, and Esc abandons a drag in progress. - Double-clicking a handle puts both its neighbours back to their declared
default-size, and reopens a collapsed one on the way. That is a different gesture from Enter on purpose: one returns a panel to where the reader left it, the other to where the author put it. - Name the splitter with
aria-label, and each handle witharia-labelwhere "Resize panel" is not enough to tell two of them apart.
API
Splitter
| Prop | Type | Default | Description |
|---|---|---|---|
ariaLabel | string | — | Accessible name, for a splitter with no visible label. |
ariaLabelledby | string | — | Ids of the elements that name the splitter. |
autoSaveId | string | — | Where the layout is remembered between visits, under ropav:splitter:<id> in localStorage. Read once after mount, and discarded unless it still matches the panels on screen. |
defaultSizes | FlexSize[] | — | Sizes the splitter starts at, when the caller is not controlling them. |
isDisabled | boolean | — | Suppresses every handle. |
keyboardLargeStep | number | 50 | How far a shift-arrow press moves a handle, in pixels. |
keyboardStep | number | 10 | How far one arrow press moves a handle, in pixels. |
orientation | "horizontal" | "vertical" | "horizontal" | Axis the panels are laid out along. A horizontal group puts its panels side by side, which makes each handle a vertical line. |
sizes | FlexSize[] | — | Every panel's size, in order. Set, a drag reports what it would do rather than doing it. |
The default slot receives:
| Slot prop | Type |
|---|---|
isDisabled | boolean |
isDragging | boolean |
layout | number[] |
orientation | SplitterOrientation |
| Event | Payload |
|---|---|
collapse | CollectionKey |
expand | CollectionKey |
resize | FlexSize[] |
resizeEnd | FlexSize[] |
resizeStart | FlexSize[] |
update:sizes | FlexSize[] |
SplitterHandle
| Prop | Type | Default | Description |
|---|---|---|---|
ariaLabel | string | "Resize panel" | Accessible name. Declared as ariaLabel, not aria-label: Vue normalises prop names, so a hyphenated declaration would never be matched. |
ariaLabelledby | string | — | Ids of the elements that name the handle. |
id | CollectionKey | — | Identity of the handle. Falls back to a generated key. |
isDisabled | boolean | — | Disables this handle alone; the splitter being disabled also counts. |
resetOnDoubleClick | boolean | true | Whether a double-click puts both neighbours back to their default size. |
showGrip | boolean | false | Draws a grip across the middle of the divider, purely decorative — the grab area reaches well past it either way. |
The default slot receives:
| Slot prop | Type |
|---|---|
isDisabled | boolean |
isDragging | boolean |
isFocused | boolean |
isFocusVisible | boolean |
isHovered | boolean |
orientation | SplitterOrientation |
SplitterPanel
| Prop | Type | Default | Description |
|---|---|---|---|
collapsedSize | FlexSize | 0 | The size a collapsed panel keeps. 0 takes it out of the layout; 48 leaves an icon rail. Cannot be given in fr. |
defaultSize | FlexSize | "1fr" | The size the panel starts at, and what a double-click on its handle restores. |
id | CollectionKey | — | Identity of the panel. Falls back to a generated key — but an explicit id is what lets a stored layout survive a panel being added, removed or reordered. |
isCollapsible | boolean | — | Whether dragging past the minimum snaps the panel shut instead of stopping there. |
maxSize | FlexSize | — | Largest size a drag may reach. Cannot be given in fr. |
minSize | FlexSize | 0 | Smallest size a drag may reach. Cannot be given in fr. |
size | FlexSize | — | A size the caller controls. A drag never writes to it. |
The default slot receives:
| Slot prop | Type |
|---|---|
isCollapsed | boolean |
isCollapsible | boolean |
orientation | SplitterOrientation |
size | number |