Component
Splitter panel
<DomSplitterPanel>A resizable multi-pane shell for editors, inspectors, file browsers, and app-builder workspaces.
Why
Use it as an application shell
A splitter rarely makes sense as an isolated widget. It is a layout primitive for tools where users need to balance navigation, work area, and detail panels without leaving the current screen.
The component keeps the center pane fluid, clamps the side panes to useful minimum widths, supports horizontal or vertical split directions, and emits size updates so an app can remember a user's preferred workspace.
Demo
Editor workspace
A file browser, visual stage, and inspector: the same shape used by the experimental template editor.
Stage
Visual component preview
The center panel stays fluid while the surrounding tools resize.
Tasks
38Risks
4Ready
82%<script setup>
import { ref } from 'vue';
import { DomSplitterPanel } from '../../../lib/vue';
const left = ref(220);
const right = ref(260);
</script>
<template>
<div class="w-full overflow-x-auto">
<DomSplitterPanel
v-model:start-size="left"
v-model:end-size="right"
class="h-[360px] min-w-[860px] overflow-hidden rounded-xl border border-border bg-background text-sm shadow-sm"
:min-start="180"
:min-main="260"
:min-end="220"
>
<template #start>
<aside class="flex h-full flex-col skin-raised">
<header class="border-b border-border px-4 py-3">
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-muted-fg">Files</p>
</header>
<nav class="space-y-1 p-3">
<button class="flex w-full items-center rounded-md bg-secondary px-3 py-2 text-left font-medium text-secondary-fg">ProjectDashboard.vue</button>
<button class="flex w-full items-center rounded-md px-3 py-2 text-left text-muted-fg hover:bg-secondary">HeroPanel.vue</button>
<button class="flex w-full items-center rounded-md px-3 py-2 text-left text-muted-fg hover:bg-secondary">TaskList.vue</button>
</nav>
</aside>
</template>
<main class="h-full min-w-0 bg-secondary/30 p-5">
<div class="mx-auto h-full max-w-xl rounded-xl border border-border bg-background p-6 shadow-sm">
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-primary">Stage</p>
<h3 class="mt-3 text-2xl font-semibold tracking-tight text-fg">Visual component preview</h3>
<p class="mt-2 text-muted-fg">The center panel stays fluid while the surrounding tools resize.</p>
<div class="mt-6 grid grid-cols-3 gap-3">
<div class="rounded-lg border border-border skin-raised p-3">
<p class="text-xs text-muted-fg">Tasks</p>
<strong class="mt-2 block text-xl text-fg">38</strong>
</div>
<div class="rounded-lg border border-border skin-raised p-3">
<p class="text-xs text-muted-fg">Risks</p>
<strong class="mt-2 block text-xl text-fg">4</strong>
</div>
<div class="rounded-lg border border-border skin-raised p-3">
<p class="text-xs text-muted-fg">Ready</p>
<strong class="mt-2 block text-xl text-fg">82%</strong>
</div>
</div>
</div>
</main>
<template #end>
<aside class="h-full skin-raised p-4">
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-muted-fg">Inspector</p>
<label class="mt-4 block text-xs font-medium text-muted-fg">Class</label>
<div class="mt-2 rounded-md border border-border bg-background px-3 py-2 font-mono text-xs text-fg">grid gap-4 p-6</div>
<label class="mt-4 block text-xs font-medium text-muted-fg">Props</label>
<div class="mt-2 space-y-2">
<div class="rounded-md border border-border bg-background px-3 py-2 text-muted-fg">title: Dashboard</div>
<div class="rounded-md border border-border bg-background px-3 py-2 text-muted-fg">status: Prototype</div>
</div>
</aside>
</template>
</DomSplitterPanel>
</div>
</template>
Demo
Two-pane review
Omit the end slot when a list/detail layout only needs one resizable side panel.
Design review
The template editor feels clearer with resizable tools.
Two-pane splitters are useful when the primary content should stay dominant, but a navigation or list panel still needs manual control.
<script setup>
import { ref } from 'vue';
import { DomSplitterPanel } from '../../../lib/vue';
const listWidth = ref(280);
</script>
<template>
<div class="w-full overflow-x-auto">
<DomSplitterPanel
v-model:start-size="listWidth"
class="h-[320px] min-w-[720px] overflow-hidden rounded-xl border border-border bg-background text-sm shadow-sm"
:min-start="220"
:min-main="360"
>
<template #start>
<aside class="h-full border-r border-border skin-raised">
<header class="border-b border-border px-4 py-3">
<p class="font-semibold text-fg">Inbox</p>
<p class="text-xs text-muted-fg">Drag the divider to widen the message list.</p>
</header>
<div class="divide-y divide-border">
<button class="block w-full bg-secondary px-4 py-3 text-left">
<span class="block font-medium text-fg">Design review</span>
<span class="mt-1 block truncate text-xs text-muted-fg">Hero component feedback and launch copy.</span>
</button>
<button class="block w-full px-4 py-3 text-left hover:bg-secondary/70">
<span class="block font-medium text-fg">Build notes</span>
<span class="mt-1 block truncate text-xs text-muted-fg">Template editor syncing checklist.</span>
</button>
<button class="block w-full px-4 py-3 text-left hover:bg-secondary/70">
<span class="block font-medium text-fg">Release plan</span>
<span class="mt-1 block truncate text-xs text-muted-fg">Tasks to close before the demo.</span>
</button>
</div>
</aside>
</template>
<article class="h-full min-w-0 overflow-y-auto p-6">
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-primary">Design review</p>
<h3 class="mt-2 text-2xl font-semibold tracking-tight text-fg">The template editor feels clearer with resizable tools.</h3>
<p class="mt-3 leading-7 text-muted-fg">
Two-pane splitters are useful when the primary content should stay dominant, but a navigation or list panel still needs manual control.
</p>
</article>
</DomSplitterPanel>
</div>
</template>
Demo
Nested layout
Combine horizontal and vertical splitters to build denser editor, console, and inspector layouts.
Preview
Nested splitters
The center workspace uses a vertical splitter while the surrounding shell keeps horizontal resize handles.
Viewport
DesktopState
Draftbuild: preview updated
layout: vertical splitter resized
sync: persisted panel sizes
status: ready
<script setup>
import { ref } from 'vue';
import { DomSplitterPanel } from '../../../lib/vue';
const files = ref(200);
const inspector = ref(240);
const preview = ref(250);
</script>
<template>
<div class="w-full overflow-x-auto">
<DomSplitterPanel
v-model:start-size="files"
v-model:end-size="inspector"
class="h-[420px] min-w-[860px] overflow-hidden rounded-xl border border-border bg-background text-sm shadow-sm"
:min-start="180"
:min-main="320"
:min-end="220"
handle-class="transition hover:bg-border/70 focus-visible:bg-ring/35 data-[dragging=true]:bg-ring/30"
>
<template #start>
<aside class="flex h-full flex-col skin-raised">
<header class="border-b border-border px-4 py-3">
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-muted-fg">Workspace</p>
</header>
<nav class="space-y-1 p-3">
<button class="w-full rounded-md bg-secondary px-3 py-2 text-left font-medium text-secondary-fg">Canvas.vue</button>
<button class="w-full rounded-md px-3 py-2 text-left text-muted-fg hover:bg-secondary">Inspector.vue</button>
<button class="w-full rounded-md px-3 py-2 text-left text-muted-fg hover:bg-secondary">Timeline.vue</button>
</nav>
</aside>
</template>
<DomSplitterPanel
v-model:start-size="preview"
orientation="vertical"
class="h-full min-w-0 bg-secondary/20"
:min-start="170"
:min-main="120"
:handle-size="10"
handle-class="transition hover:bg-border/70 focus-visible:bg-ring/35 data-[dragging=true]:bg-ring/30"
>
<template #start>
<main class="h-full min-w-0 overflow-auto p-5">
<div class="h-full rounded-lg border border-border bg-background p-5 shadow-sm">
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-primary">Preview</p>
<h3 class="mt-3 text-xl font-semibold text-fg">Nested splitters</h3>
<p class="mt-2 max-w-md leading-6 text-muted-fg">
The center workspace uses a vertical splitter while the surrounding shell keeps horizontal resize handles.
</p>
<div class="mt-5 grid grid-cols-2 gap-3">
<div class="rounded-md border border-border skin-raised p-3">
<p class="text-xs text-muted-fg">Viewport</p>
<strong class="mt-1 block text-fg">Desktop</strong>
</div>
<div class="rounded-md border border-border skin-raised p-3">
<p class="text-xs text-muted-fg">State</p>
<strong class="mt-1 block text-fg">Draft</strong>
</div>
</div>
</div>
</main>
</template>
<section class="h-full overflow-auto border-t border-border bg-background p-4 font-mono text-xs leading-6 text-muted-fg">
<p class="text-fg">build: preview updated</p>
<p>layout: vertical splitter resized</p>
<p>sync: persisted panel sizes</p>
<p>status: ready</p>
</section>
</DomSplitterPanel>
<template #end>
<aside class="h-full overflow-auto skin-raised p-4">
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-muted-fg">Inspector</p>
<div class="mt-4 space-y-3">
<label class="block text-xs font-medium text-muted-fg">Width</label>
<div class="rounded-md border border-border bg-background px-3 py-2 text-fg">fluid</div>
<label class="block text-xs font-medium text-muted-fg">Height</label>
<div class="rounded-md border border-border bg-background px-3 py-2 text-fg">split</div>
<label class="block text-xs font-medium text-muted-fg">Handle</label>
<div class="rounded-md border border-border bg-background px-3 py-2 text-fg">transparent</div>
</div>
</aside>
</template>
</DomSplitterPanel>
</div>
</template>
Demo
Data workbench
Three-pane layouts are useful for database browsers, admin tools, and API consoles.
| Project | Owner | Status |
|---|---|---|
| Launch OS | Product | Prototype |
| Editor shell | Design | Review |
| Data sync | Platform | Planned |
<script setup>
import { ref } from 'vue';
import { DomSplitterPanel } from '../../../lib/vue';
const side = ref(220);
const details = ref(260);
</script>
<template>
<div class="w-full overflow-x-auto">
<DomSplitterPanel
v-model:start-size="side"
v-model:end-size="details"
class="h-[340px] min-w-[820px] overflow-hidden rounded-xl border border-border bg-background text-sm shadow-sm"
:min-start="180"
:min-main="260"
:min-end="220"
>
<template #start>
<aside class="h-full skin-raised p-4">
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-muted-fg">Sources</p>
<div class="mt-4 space-y-2">
<div class="rounded-md bg-secondary px-3 py-2 font-medium text-secondary-fg">projects</div>
<div class="rounded-md px-3 py-2 text-muted-fg">tasks</div>
<div class="rounded-md px-3 py-2 text-muted-fg">users</div>
</div>
</aside>
</template>
<main class="h-full min-w-0 overflow-auto p-5">
<table class="w-full min-w-[420px] text-left">
<thead class="text-xs uppercase tracking-[0.14em] text-muted-fg">
<tr>
<th class="border-b border-border py-2">Project</th>
<th class="border-b border-border py-2">Owner</th>
<th class="border-b border-border py-2">Status</th>
</tr>
</thead>
<tbody class="text-fg">
<tr>
<td class="border-b border-border py-3">Launch OS</td>
<td class="border-b border-border py-3">Product</td>
<td class="border-b border-border py-3">Prototype</td>
</tr>
<tr>
<td class="border-b border-border py-3">Editor shell</td>
<td class="border-b border-border py-3">Design</td>
<td class="border-b border-border py-3">Review</td>
</tr>
<tr>
<td class="border-b border-border py-3">Data sync</td>
<td class="border-b border-border py-3">Platform</td>
<td class="border-b border-border py-3">Planned</td>
</tr>
</tbody>
</table>
</main>
<template #end>
<aside class="h-full skin-raised p-4">
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-muted-fg">Row details</p>
<h3 class="mt-4 font-semibold text-fg">Launch OS</h3>
<p class="mt-2 leading-6 text-muted-fg">Resizable detail panes work well for database browsers, API explorers, and admin tools.</p>
</aside>
</template>
</DomSplitterPanel>
</div>
</template>
Usage
Vue
<script setup>
import { ref } from 'vue';
import { DomSplitterPanel } from '@getdom/studio/vue';
const left = ref(280);
const right = ref(360);
</script>
<template>
<DomSplitterPanel
v-model:start-size="left"
v-model:end-size="right"
class="h-screen"
:min-start="220"
:min-main="560"
:min-end="320"
handle-class="transition hover:bg-border/70 data-[dragging=true]:bg-ring/30"
>
<template #start>
<FileBrowser />
</template>
<VisualStage />
<template #end>
<InspectorPanel />
</template>
</DomSplitterPanel>
</template>Usage
Vertical splitter
<script setup>
import { ref } from 'vue';
import { DomSplitterPanel } from '@getdom/studio/vue';
const previewHeight = ref(420);
</script>
<template>
<DomSplitterPanel
v-model:start-size="previewHeight"
orientation="vertical"
class="h-full"
:min-start="240"
:min-main="160"
:handle-size="10"
>
<template #start>
<PreviewCanvas />
</template>
<ConsolePanel />
</DomSplitterPanel>
</template>Reference
Props
Control props
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
orientation | 'horizontal' | 'vertical' | string | 'horizontal' | Resize direction. Horizontal creates columns; vertical creates rows. |
startSize | number | number | 280 | Initial width or height of the leading pane in pixels. |
endSize | number | number | 360 | Initial width or height of the trailing pane in pixels. |
minStart | number | number | 180 | Minimum leading pane width or height. |
minMain | number | number | 320 | Minimum central pane width or height. |
minEnd | number | number | 220 | Minimum trailing pane width or height. |
handleSize | number | number | 8 | Splitter handle hit area in pixels. |
handleClass | string | array | object | Array<unknown> | 'transition hover:bg-ring/25 focus-visible:bg-ring/35 data-[dragging=true]:bg-ring/35' | Classes applied to each resize button. Use this to make handles transparent, bordered, or prominent. |
activeHandleClass | string | array | object | Array<unknown> | '' | Additional classes applied to the active resize button while dragging. |
handleIndicatorClass | string | array | object | Array<unknown> | '' | Classes applied to the small visual indicator inside each resize button. |
Auto-generated from Splitter panel.props and inline _edit hints.
Events
| Name | Payload | Description |
|---|---|---|
| @update:startSize | (pixels) | Emitted when the leading pane is resized. |
| @update:endSize | (pixels) | Emitted when the trailing pane is resized. |
| @resize | ({ startSize, endSize }) | Emitted after either splitter handle moves. |
Names auto-detected from defineEmits and source emit() calls; payload and description from __doc.events when present.
Slots
| Name | Scope | Description |
|---|---|---|
| #start | — | The leading resizable pane. |
| #default | — | The flexible central pane. |
| #end | — | Optional trailing resizable pane. |
| #start-handle | — | Optional content inside the leading resize handle. |
| #end-handle | — | Optional content inside the trailing resize handle. |