---
title: Splitter - Resizable Panel Layout with Drag Handles
meta:
- name: description
  content: Headless splitter component with resizable panels and draggable handles. Supports keyboard navigation, orientation control, and nested layouts for Vue 3.
- name: keywords
  content: splitter, resizable, panels, drag handle, resize, layout, Vue 3, headless, accessible
features:
  category: Component
  label: 'C: Splitter'
  github: /components/Splitter/
  renderless: false
  level: 2
related:
  - /components/primitives/atom
---

# Splitter

A headless component for building resizable panel layouts with drag handles and full keyboard support.

<DocsPageFeatures :frontmatter />

## Usage

The Splitter provides resizable panels separated by draggable handles. Panel sizes are specified as percentages and must sum to 100.

::: gn-example
/components/splitter/basic
:::

## Anatomy

```vue Anatomy no-filename
<script setup lang="ts">
  import { Splitter } from '@vuetify/v0'
</script>

<template>
  <Splitter.Root>
    <Splitter.Panel />

    <Splitter.Handle />

    <Splitter.Panel />
  </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.

::: gn-example
/components/splitter/resize-handle.vue 1
/components/splitter/playground.vue 2

### 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.Root` uses the default horizontal orientation for the sidebar/content split
- The inner `Splitter.Root` sets `orientation="vertical"` to stack the editor above the preview
- `min-size` and `max-size` constraints on the sidebar panel (15–30%) prevent it from collapsing or dominating the layout
- `ResizeHandle` is a thin wrapper around `Splitter.Handle` — it takes a `horizontal` prop rather than reading context, making it portable across any splitter without coupling to a specific root
:::

## Recipes

### Orientation

Set `orientation` on the root to control layout direction. Defaults to `horizontal`.

```vue
<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.

Pointer drag pins at `minSize` (or `collapsedSize` when opening), arms a pending intent after a small overshoot, commits on pointer **release**, and cancels if you drag back (or the gesture is cancelled). Keyboard Home/End/Enter stay **immediate**. Handle slot `pending` is `'collapse' | 'expand' | null`; `data-pending` matches; `pendingLabel` is the localized “Release to hide/open” string. Home/End/Enter affect the **preceding** (`aria-controls`) panel; arrow keys and pointer pending can affect either adjacent collapsible panel.

```vue
<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.

```vue
<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. Pointer drag commits collapse/expand on **release**; keyboard Home/End is instant.

### Events

The root emits `@layout` with all panel sizes at the end of each resize interaction. Panels emit `@resize` with their individual size.

```vue
<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.

```vue
<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.

```vue
<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](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/) pattern.

- Each handle has `role="separator"` with `aria-valuenow`, `aria-valuemin`, and `aria-valuemax`
- `aria-orientation` is set **perpendicular** to the layout direction (a horizontal layout produces vertical separators)
- `aria-controls` links each handle to the panel it precedes
- Use the `label` prop on handles to provide an `aria-label` (e.g., `label="Resize sidebar"`)
- Disabled handles set `tabindex="-1"` and `aria-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. Home/End/Enter affect the **preceding** (`aria-controls`) panel; arrow keys and pointer pending can affect either adjacent collapsible panel.

## FAQ

::: faq

??? How do I set the initial panel sizes?

Panel sizes are percentages set via `default-size` and must sum to 100. Constrain them with `min-size` and `max-size` so a panel can't collapse or dominate.

??? How do I control collapse from outside the splitter?

Use `v-model:collapsed` on the panel for two-way binding — setting the ref collapses or expands it. Pointer drag commits on **release**; keyboard Home/End is instant. For inline controls, the panel's slot props expose `collapse()`, `expand()`, and `isCollapsed`.

??? How do I resize panels programmatically?

Call `distribute([...])` from the root's slot props to set all sizes at once; values are clamped to each panel's min/max. To react to user resizes, listen to `@layout` (all sizes) or a panel's `@resize`.

??? How do I make a vertical splitter?

Set `orientation="vertical"` on `Splitter.Root` to stack panels top-to-bottom; the default is `horizontal`.

??? Can I nest splitters?

Yes. Place a `Splitter.Root` inside any `Splitter.Panel` — each nested splitter manages its own sizes, handles, and orientation independently of the outer one.

??? How do I resize a panel with the keyboard?

Focus a handle, then use the arrow keys to resize by 1% and Page Up / Page Down by 10%. Home and End collapse or expand the preceding panel, and Enter toggles its collapse state.

:::

<DocsApi />
