Splitter
Accessible, unstyled resizable panel groups following the WAI-ARIA window splitter pattern.
|
|
| Package |
@vizejs/ui/splitter |
| Maturity |
stable |
| Own the source |
vize lib pull splitter |
| Requires |
collection, context, controllable-state, id |
| Aliases |
resizable panels, split view, split pane, window splitter, panel group |
| Covers |
WAI-ARIA window splitter pattern, react-resizable-panels, Reka UI Splitter, Ark UI Splitter |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { SplitterGroup, SplitterHandle, SplitterPanel } from "@vizejs/ui/splitter";
Or copy the source into your project with vize lib pull splitter (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Two-pane horizontal splitter with a collapsible sidebar and a labelled resize handle. -->
<script setup lang="ts">
import { SplitterGroup, SplitterHandle, SplitterPanel } from "@vizejs/ui/splitter";
</script>
<template>
<SplitterGroup>
<SplitterPanel :default-size="30" :min-size="20" collapsible>
<nav aria-label="Folders">
<ul>
<li>Inbox</li>
<li>Archive</li>
</ul>
</nav>
</SplitterPanel>
<SplitterHandle aria-label="Resize folder list" />
<SplitterPanel :default-size="70" :min-size="40">
<p>Select a folder to read its messages.</p>
</SplitterPanel>
</SplitterGroup>
</template>
API
SplitterGroup
Source: src/families/layout/splitter/splitter-group.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned group id. null and undefined select a deterministic fallback. |
layout |
SplitterLayout |
undefined |
Controlled panel sizes in percent (v-model:layout). undefined selects uncontrolled behavior, which also lets useSplitterPersistence load a stored layout after mount. |
defaultLayout |
SplitterLayout |
undefined |
Initial sizes by panel index. Panels without an entry use their defaultSize. |
orientation |
SplitterOrientation |
"horizontal" |
Layout axis. "horizontal" places panels side by side with vertical separators. |
dir |
SplitterDirection |
undefined |
Reading direction used to map horizontal arrow keys and pointer movement. undefined inherits DirectionProvider/LocaleProvider, then "ltr". |
disabled |
boolean |
false |
Disable pointer and keyboard resizing while keeping the current layout. |
keyboardStep |
number |
10 |
Percent moved by one arrow key press on a handle. |
Events
| Event |
Payload |
Description |
update:layout |
[layout: SplitterLayout] |
Fired with the next layout whenever sizes change. |
resize |
[layout: SplitterLayout, reason: SplitterResizeReason] |
Fired after a distinct layout change with the input that caused it. |
resizeStart |
[handleIndex: number] |
Fired when a pointer drag starts on a handle. |
resizeEnd |
[layout: SplitterLayout] |
Fired when a pointer drag ends. |
Slots
| Slot |
Slot props |
Description |
default |
SplitterGroupSlotState |
SplitterPanel and SplitterHandle children. Receives the resolved layout and drag state. |
Exposed
| Member |
Type |
Description |
element |
HTMLDivElement | null |
Rendered group element. |
id |
string |
Group base id. |
layout |
SplitterLayout |
Resolved layout. |
setLayout |
(layout: SplitterLayout) => boolean |
Request a new layout. Invalid lengths are rejected and sizes are clamped to constraints. |
reset |
() => boolean |
Restore defaultLayout or the panel defaults. |
SplitterHandle
Source: src/families/layout/splitter/splitter-handle.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned separator id. |
disabled |
boolean |
false |
Disable this handle while keeping the rest of the group resizable. |
order |
number |
undefined |
Deterministic order for conditionally rendered handles. |
ariaLabel |
string |
undefined |
Accessible name, for example "Resize sidebar". |
ariaLabelledby |
string |
undefined |
Space-separated ids that label the separator. |
Slots
| Slot |
Slot props |
Description |
default |
SplitterHandleSlotState |
Optional grip content. Receives the value, orientation, and drag state. |
Exposed
| Member |
Type |
Description |
element |
HTMLDivElement | null |
Rendered separator element. |
value |
number |
Size in percent of the panel before the handle. |
focus |
(options?: FocusOptions) => void |
Move focus to the separator. |
SplitterPanel
Source: src/families/layout/splitter/splitter-panel.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned panel id, also used by aria-controls on the adjacent handle. |
defaultSize |
number |
undefined |
Initial size in percent. Give every panel a size for exact server rendering. |
minSize |
number |
0 |
Smallest expanded size in percent. |
maxSize |
number |
100 |
Largest size in percent. |
collapsible |
boolean |
false |
Allow the panel to snap to collapsedSize when dragged past half of minSize. |
collapsedSize |
number |
0 |
Size in percent while collapsed. |
order |
number |
undefined |
Deterministic order for conditionally rendered panels. |
Events
| Event |
Payload |
Description |
collapse |
[] |
Fired when the panel snaps to its collapsed size. |
expand |
[] |
Fired when a collapsed panel grows back to at least its minimum size. |
resize |
[size: number, previous: number] |
Fired with the panel's new size whenever it changes. |
Slots
| Slot |
Slot props |
Description |
default |
SplitterPanelSlotState |
Panel content. Receives the current size and collapsed state. |
Exposed
| Member |
Type |
Description |
element |
HTMLDivElement | null |
Rendered panel element. |
id |
string |
Panel id. |
size |
number |
Current size in percent. |
collapsed |
boolean |
Whether the panel is collapsed. |
collapse |
() => boolean |
Collapse a collapsible panel, remembering its size. |
expand |
() => boolean |
Restore a collapsed panel to its remembered or minimum size. |
resize |
(size: number) => boolean |
Resize the panel toward size percent through its adjacent handle. |
Behavior
Normative state x input -> outcome table for splitter-group.vue,
splitter-panel.vue, and splitter-handle.vue (@vizejs/ui/splitter), plus
the useSplitterPersistence hook and the pure layout helpers. Every row is
proven by the named test in splitter.test.ts, splitter-layout.test.ts, or
splitter-ssr.test.ts; compile-only guarantees live in
splitter.types.test-d.ts.
The handle follows the WAI-ARIA APG window splitter pattern: a focusable
role="separator" whose aria-valuenow is the size of the panel before it
(its primary pane, named by aria-controls) and whose aria-orientation is
perpendicular to the group axis. Sizes are percentages of the group and the
only inline styles are the flex declarations that carry them.
| ID |
State |
Input |
Outcome |
Evidence |
| S1 |
default |
render |
flex group, flex-grow sizes, separator value/min/max/controls, perpendicular orientation |
renders the APG window splitter contract with flex-carried sizes |
| S2 |
focused handle |
ArrowLeft/Right, Home, End |
resize by keyboardStep inside min/max; Home/End jump to the primary pane's bounds |
arrow keys resize by the keyboard step within constraints; Home and End jump |
| S3 |
vertical group / RTL |
ArrowUp/Down, ArrowLeft |
vertical groups use block arrows; RTL flips inline arrows |
vertical groups use ArrowUp/ArrowDown and RTL flips horizontal arrows |
| S4 |
collapsible primary pane |
Enter, Home, ArrowRight |
Enter collapses and restores the remembered size; collapsed panes grow straight to minSize |
collapsible panels snap closed past half their minimum and Enter toggles them |
| S5 |
pointer drag |
pointerdown, move, up |
sizes follow the pointer from the drag origin, clamp to constraints, emit start/end |
pointer drags resize from the drag origin and report start and end |
| S6 |
disabled group |
keys, pointer |
no resize, separator leaves the tab order and reports aria-disabled |
disabled groups and handles ignore keyboard and pointer input |
| S7 |
controlled layout |
resize, invalid layout |
update events fire while the parent owns sizes; wrong panel counts fall back to defaults |
controlled layouts wait for the parent and invalid layouts fall back to defaults |
| S8 |
exposed instances |
setLayout, collapse, expand, resize, reset |
typed imperative layout control with validation |
exposes group and panel controls for programmatic layout changes |
| S9 |
nested groups |
keys on the inner handle |
each group owns its own panels and handles |
nested groups resize independently |
| S10 |
useSplitterPersistence |
mount, resize, malformed storage |
defaults render first, the stored layout loads after mount, changes are written back |
useSplitterPersistence loads after mount and writes later layouts |
| S11 |
missing provider |
setup |
panels and handles fail closed with the shared context diagnostic |
compound parts require a matching group provider |
| S12 |
layout helpers |
validate, default, clamp, resize |
remainder sharing, clamping, cascading resize, and midpoint collapse snapping |
splitter-layout.test.ts |
| S13 |
SSR and hydration |
isolated render/mount |
byte-identical exact sizes, controlled layouts before registration, hydration without warnings |
splitter-ssr.test.ts |
Notes
- For exact server rendering give every panel a
defaultSize, or pass
defaultLayout/layout to the group; unsized panels share the remainder
once all panels have registered.
- Resizing only moves the boundary between the handle's neighbours, cascading
through further panels when a neighbour reaches its minimum.