SvSplitter
Two resizable panes divided by a draggable WAI-ARIA separator, with pointer drag and arrow-key resize.
SvSplitter renders a start and an end snippet on either side of a gutter you
can drag or resize with the keyboard. It is controlled through fraction (the
first pane's share of the container, 0..1) and onChange. The clamp, keyboard,
ARIA, and RTL logic live in a headless createSplitter core; the component owns
only the container measurement. Nest splitters for IDE- or dashboard-style layouts.
Related: SvTabs · SvScrollArea · Layout & composite overview
Installation
Add it with the CLI - this drops a ready-to-edit SvSplitter starter into your app:
Prefer to see it first? npx @svgrid/ui try splitter opens it in a throwaway sandbox - no project needed.
Or install the package and import it directly. SvSplitter ships free in
@svgrid/grid (dependency-free):
The examples on this page import from @svgrid/grid:
<script lang="ts">
import { SvScrollArea, SvSplitter } from '@svgrid/grid'
</script>
import { SvSplitter } from '@svgrid/grid'
Example
Open the live example: Splitter (Layout)
<script lang="ts">
import { SvSplitter } from '@svgrid/grid'
let fraction = $state(0.3)
</script>
<div style="height: 320px">
<SvSplitter {fraction} onChange={(f) => (fraction = f)} ariaLabel="Sidebar width">
{#snippet start()}<nav>…tree…</nav>{/snippet}
{#snippet end()}<main>…content…</main>{/snippet}
</SvSplitter>
</div>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
fraction |
number |
0.5 |
First pane size as a fraction of the container (bindable). |
onChange |
(fraction: number) => void |
- | Fired as the separator moves. |
orientation |
SplitterOrientation (horizontal | vertical) |
horizontal |
horizontal = left/right panes; vertical = top/bottom. |
min |
number |
0.1 |
Minimum fraction for the first pane. |
max |
number |
0.9 |
Maximum fraction for the first pane. |
step |
number |
0.02 |
Keyboard resize step as a fraction (2%). |
disabled |
boolean |
false |
Locks the separator. |
dir |
EditorDir (ltr | rtl | auto) |
- | Text direction. rtl flips the horizontal drag direction. |
ariaLabel |
string |
- | Accessible name for the separator. |
start |
Snippet |
- | Content of the first pane. |
end |
Snippet |
- | Content of the second pane. |
SplitterOrientation
type SplitterOrientation = 'horizontal' | 'vertical'
Examples
Bounded sidebar
Constrain the drag range so a sidebar cannot be collapsed away or dragged too wide:
<SvSplitter {fraction} min={0.15} max={0.4}
onChange={(f) => (fraction = f)} ariaLabel="Sidebar" />
Nested (IDE layout)
Put a vertical splitter inside one pane of a horizontal one for an editor plus
a stacked panel and terminal. Each splitter targets its own separator, so a nested
splitter of the opposite orientation sizes correctly.
Resizable master / detail
A record list on the left and the selected record on the right, with a divider the
user can drag. Keep fraction in $state and let each pane scroll on its own:
<script lang="ts">
import { SvSplitter, SvScrollArea } from '@svgrid/grid'
let split = $state(0.32)
let selected = $state(rows[0])
</script>
<div style="height: 420px">
<SvSplitter fraction={split} min={0.2} max={0.5}
onChange={(f) => (split = f)} ariaLabel="List width">
{#snippet start()}
<SvScrollArea height="100%">
{#each rows as row}
<button onclick={() => (selected = row)}>{row.name}</button>
{/each}
</SvScrollArea>
{/snippet}
{#snippet end()}
<SvScrollArea height="100%"><RecordDetail record={selected} /></SvScrollArea>
{/snippet}
</SvSplitter>
</div>
Tip: the separator is keyboard-resizable - arrow keys move it by step
(default 2%) and Home / End jump to min / max - so users who cannot drag
can still size the panes.
Accessibility
- The gutter has
role="separator"witharia-orientation,aria-valuemin/aria-valuemax/aria-valuenow, and a focusable tabindex. - Arrow keys resize by
step;Home/Endjump to themin/maxbounds. - Pointer drags use pointer capture so a fast drag outside the element keeps
tracking;
disabledblocks both pointer and keyboard resize.
A resizable pane
fraction is a 0-1 split rather than a pixel width, so the panes keep their
proportion when the window resizes. min and max stop either side being
dragged to nothing.
<script lang="ts">
import { SvSplitter, SvGrid, type GridColumns } from '@svgrid/grid'
type Person = { name: string; city: string }
const people: Person[] = [
{ name: 'Ada Lovelace', city: 'London' },
{ name: 'Grace Hopper', city: 'New York' },
]
const columns: GridColumns<Person> = [
{ field: 'name', header: 'Name', width: 160 },
{ field: 'city', header: 'City', width: 120 },
]
let split = $state(0.6)
</script>
<div style="height: 260px;">
<SvSplitter fraction={split} onChange={(f) => (split = f)} min={0.25} max={0.8}>
{#snippet start()}
<SvGrid data={people} {columns} />
{/snippet}
{#snippet end()}
<div style="padding: 12px;">Detail pane - {Math.round(split * 100)}% / {Math.round((1 - split) * 100)}%</div>
{/snippet}
</SvSplitter>
</div>
See also
- SvTabs - swap panels within one pane instead of showing both.
- SvScrollArea - themed scrollbars for content inside a pane.
- Layout overview - the whole layout family at a glance.