Splitter
A headless component for building resizable panel layouts with drag handles and full keyboard support.
Usage
The Splitter provides resizable panels separated by draggable handles. Panel sizes are specified as percentages and must sum to 100.
<script setup lang="ts">
import { Splitter } from '@vuetify/v0'
import ResizeHandle from './resize-handle.vue'
</script>
<template>
<Splitter.Root class="h-48 border border-divider rounded-lg overflow-hidden">
<Splitter.Panel
class="flex items-center justify-center bg-surface"
:default-size="50"
:min-size="20"
>
<span class="text-sm text-on-surface-variant">Panel A</span>
</Splitter.Panel>
<ResizeHandle label="Resize panels" />
<Splitter.Panel
class="flex items-center justify-center bg-surface"
:default-size="50"
:min-size="20"
>
<span class="text-sm text-on-surface-variant">Panel B</span>
</Splitter.Panel>
</Splitter.Root>
</template>
Anatomy
<script setup lang="ts">
import { Splitter } from '@vuetify/v0'
</script>
<template>
<Splitter.Root>
<Splitter.Panel :default-size="50" />
<Splitter.Handle />
<Splitter.Panel :default-size="50" />
</Splitter.Root>
</template>Examples
Nested Layouts
Splitters compose naturally — place a Splitter.Root inside any panel to build complex layouts. Each nested splitter operates independently with its own sizes, handles, and orientation, while the outer splitter manages the top-level split.
This IDE-style workspace demonstrates the pattern with two levels of nesting: a horizontal splitter divides the sidebar from the main content area, and a vertical splitter inside the content panel separates the code editor from a live preview.
Playground Layout
An IDE workspace split across two files — the layout composition and a reusable handle component:
| File | Role |
|---|---|
playground.vue | Composes the nested layout — horizontal sidebar split with a vertical editor/preview split inside |
resize-handle.vue | Reusable styled handle that accepts an horizontal prop to adapt its cursor, dimensions, and grip indicator direction |
Key patterns:
The outer
Splitter.Rootuses the default horizontal orientation for the sidebar/content splitThe inner
Splitter.Rootsetsorientation="vertical"to stack the editor above the previewmin-sizeandmax-sizeconstraints on the sidebar panel (15–30%) prevent it from collapsing or dominating the layoutResizeHandleis a thin wrapper aroundSplitter.Handle— it takes ahorizontalprop rather than reading context, making it portable across any splitter without coupling to a specific root
<template> <h1>Hello World</h1> </template>
Hello World
Recipes
Orientation
Set orientation on the root to control layout direction. Defaults to horizontal.
<template>
<Splitter.Root orientation="vertical">
<Splitter.Panel :default-size="50">Top</Splitter.Panel>
<Splitter.Handle />
<Splitter.Panel :default-size="50">Bottom</Splitter.Panel>
</Splitter.Root>
</template>Collapsible Panels
Panels can collapse to a minimum size. Set collapsible and optionally collapsed-size on the panel. The panel’s slot props provide collapse(), expand(), size, and isCollapsed — use these to build collapse controls inline. Keyboard users can press Home/End on the adjacent handle.
<script setup lang="ts">
import { Splitter } from '@vuetify/v0'
</script>
<template>
<Splitter.Root>
<Splitter.Panel
v-slot="{ collapse, expand, isCollapsed }"
:default-size="30"
:min-size="15"
:collapsed-size="0"
collapsible
>
<button v-if="isCollapsed" @click="expand">Expand</button>
<template v-else>
<button @click="collapse">Collapse</button>
Sidebar
</template>
</Splitter.Panel>
<Splitter.Handle label="Resize sidebar" />
<Splitter.Panel :default-size="70" :min-size="30">
Content
</Splitter.Panel>
</Splitter.Root>
</template>Controlled Collapse
Use v-model:collapsed for two-way binding of collapsed state. This lets you control collapse from outside the splitter — for example, a toolbar button or a shared ref.
<script setup lang="ts">
import { Splitter } from '@vuetify/v0'
import { shallowRef } from 'vue'
const collapsed = shallowRef(false)
</script>
<template>
<button @click="collapsed = !collapsed">
{{ collapsed ? 'Show' : 'Hide' }} Sidebar
</button>
<Splitter.Root>
<Splitter.Panel
v-model:collapsed="collapsed"
:default-size="30"
:min-size="15"
:collapsed-size="0"
collapsible
>
Sidebar
</Splitter.Panel>
<Splitter.Handle label="Resize sidebar" />
<Splitter.Panel :default-size="70" :min-size="30">
Content
</Splitter.Panel>
</Splitter.Root>
</template>The model syncs in both directions — setting the ref collapses/expands the panel, and drag-to-collapse or keyboard Home/End updates the ref.
Events
The root emits @layout with all panel sizes at the end of each resize interaction. Panels emit @resize with their individual size.
<template>
<Splitter.Root @layout="sizes => console.log('layout', sizes)">
<Splitter.Panel :default-size="50" @resize="size => console.log('panel', size)">
Left
</Splitter.Panel>
<Splitter.Handle />
<Splitter.Panel :default-size="50">Right</Splitter.Panel>
</Splitter.Root>
</template>Programmatic Sizing
Use distribute() from the root’s slot props to set all panel sizes at once. Values are clamped to each panel’s min/max constraints. Place controls inside a panel to keep them out of the root’s flex layout.
<script setup lang="ts">
import { Splitter } from '@vuetify/v0'
</script>
<template>
<Splitter.Root v-slot="{ distribute }">
<Splitter.Panel :default-size="50" :min-size="20">
<button @click="distribute([30, 70])">30 / 70</button>
<button @click="distribute([50, 50])">50 / 50</button>
Left
</Splitter.Panel>
<Splitter.Handle />
<Splitter.Panel :default-size="50" :min-size="20">Right</Splitter.Panel>
</Splitter.Root>
</template>Disabled State
Disable all resize interactions via the disabled prop on the root, or disable individual handles.
<template>
<!-- Disable all handles -->
<Splitter.Root disabled>
...
</Splitter.Root>
<!-- Disable a single handle -->
<Splitter.Root>
<Splitter.Panel :default-size="33" />
<Splitter.Handle disabled />
<Splitter.Panel :default-size="34" />
<Splitter.Handle />
<Splitter.Panel :default-size="33" />
</Splitter.Root>
</template>Accessibility
The Splitter implements the WAI-ARIA Window Splitter↗ pattern.
Each handle has
role="separator"witharia-valuenow,aria-valuemin, andaria-valuemaxaria-orientationis set perpendicular to the layout direction (a horizontal layout produces vertical separators)aria-controlslinks each handle to the panel it precedesUse the
labelprop on handles to provide anaria-label(e.g.,label="Resize sidebar")Disabled handles set
tabindex="-1"andaria-disabled="true"
Keyboard Navigation
| Key | Action |
|---|---|
| Arrow Left / Arrow Up | Shrink preceding panel by 1% |
| Arrow Right / Arrow Down | Grow preceding panel by 1% |
| Page Up | Shrink preceding panel by 10% |
| Page Down | Grow preceding panel by 10% |
| Home | Collapse preceding panel (if collapsible) or shrink to minimum |
| End | Expand preceding panel (if collapsed) or grow to maximum |
| Enter | Toggle collapse state of preceding panel (if collapsible) |
Arrow direction follows the layout orientation — horizontal splitters use Left/Right, vertical splitters use Up/Down.
Splitter.Root
Props
Events
layout
[sizes: number[]]Slots
default
SplitterRootSlotPropsSplitter.Handle
Props
Slots
default
SplitterHandleSlotPropsSplitter.Panel
Props
Events
update:collapsed
unknown[]resize
[size: number]Slots
default
SplitterPanelSlotProps