Usage
Use the Splitter component to display a list of resizable panels separated by draggable handles.
<script setup lang="ts">
import type { SplitterItem } from '@nuxt/ui'
const card = 'bg-elevated/50 border border-default rounded-xl items-center justify-center text-muted font-medium'
const items: SplitterItem[] = [
{ slot: 'left', minSize: 15, defaultSize: 25, class: card },
{ slot: 'main', minSize: 30, defaultSize: 50, class: card },
{ slot: 'right', minSize: 15, defaultSize: 25, class: card }
]
</script>
<template>
<div class="w-full h-96">
<USplitter id="splitter-example" :items="items">
<template #left>
Left
</template>
<template #main>
Main
</template>
<template #right>
Right
</template>
</USplitter>
</div>
</template>
Items
Use the items prop as an array of objects with the following properties:
defaultSize?: numberminSize?: numbermaxSize?: numbercollapsible?: booleancollapsedSize?: numbersizeUnit?: '%' | 'px'order?: numberid?: stringslot?: stringclass?: anyui?: { panel?: ClassNameValue }
Use the slot key to fill the content of a panel and the class key to style it. Items without a slot key fall back to a panel-{index} slot. Sizes are percentages by default, set sizeUnit: 'px' on an item for pixel values.
id prop and give defaultSize to all items or to none. Ids are generated automatically otherwise and the server and the client can disagree, which breaks the layout on hydration. An item without a defaultSize falls back to an equal share on the server, so mixing the two makes panels jump once hydrated. Pixel sizes are measured on the client and always shift a little.<script setup lang="ts">
import type { SplitterItem } from '@nuxt/ui'
const items = ref<SplitterItem[]>([
{
slot: 'sidebar',
minSize: 15,
maxSize: 40,
defaultSize: 25,
class:
'bg-elevated/50 border border-default rounded-xl items-center justify-center text-muted font-medium'
},
{
slot: 'main',
defaultSize: 75,
class:
'bg-elevated/50 border border-default rounded-xl items-center justify-center text-muted font-medium'
}
])
</script>
<template>
<USplitter id="splitter-items" :items="items">
<template #sidebar> Sidebar </template>
<template #main> Main </template>
</USplitter>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { SplitterItem } from '@nuxt/ui'
const items = ref<SplitterItem[]>([
{
slot: 'sidebar',
minSize: 15,
maxSize: 40,
defaultSize: 25,
class:
'bg-elevated/50 border border-default rounded-xl items-center justify-center text-muted font-medium'
},
{
slot: 'main',
defaultSize: 75,
class:
'bg-elevated/50 border border-default rounded-xl items-center justify-center text-muted font-medium'
}
])
</script>
<template>
<USplitter id="splitter-items" :items="items">
<template #sidebar> Sidebar </template>
<template #main> Main </template>
</USplitter>
</template>
Orientation
Use the orientation prop to change the direction of the splitter. Defaults to horizontal.
<script setup lang="ts">
import type { SplitterItem } from '@nuxt/ui'
const items = ref<SplitterItem[]>([
{
slot: 'first',
class:
'bg-elevated/50 border border-default rounded-xl items-center justify-center text-muted font-medium'
},
{
slot: 'second',
class:
'bg-elevated/50 border border-default rounded-xl items-center justify-center text-muted font-medium'
}
])
</script>
<template>
<USplitter id="splitter-orientation" orientation="vertical" :items="items">
<template #first> First </template>
<template #second> Second </template>
</USplitter>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { SplitterItem } from '@nuxt/ui'
const items = ref<SplitterItem[]>([
{
slot: 'first',
class:
'bg-elevated/50 border border-default rounded-xl items-center justify-center text-muted font-medium'
},
{
slot: 'second',
class:
'bg-elevated/50 border border-default rounded-xl items-center justify-center text-muted font-medium'
}
])
</script>
<template>
<USplitter id="splitter-orientation" orientation="vertical" :items="items">
<template #first> First </template>
<template #second> Second </template>
</USplitter>
</template>
Examples
With collapsible panel
Set collapsible: true on an item to let it collapse past its minSize, and use collapsedSize to keep part of the panel visible when collapsed. The panel slot exposes collapsed, collapse and expand so you can control it programmatically, and the collapse, expand and resize events fire with the panel index.
<script setup lang="ts">
import type { SplitterItem } from '@nuxt/ui'
const items: SplitterItem[] = [
{ slot: 'sidebar', sizeUnit: 'px', minSize: 150, defaultSize: 250, collapsible: true, collapsedSize: 48, class: 'bg-elevated/50 border border-default rounded-xl' },
{ slot: 'main', class: 'bg-elevated/50 border border-default rounded-xl items-center justify-center text-muted font-medium' }
]
</script>
<template>
<div class="w-full h-96">
<USplitter id="splitter-collapsible-example" :items="items">
<template #sidebar="{ collapsed, collapse, expand }">
<div class="flex-1 flex items-center justify-center p-2">
<UButton
:icon="collapsed ? 'i-lucide-panel-left-open' : 'i-lucide-panel-left-close'"
:label="collapsed ? undefined : 'Collapse'"
:aria-label="collapsed ? 'Expand' : undefined"
color="neutral"
variant="subtle"
@click="collapsed ? expand() : collapse()"
/>
</div>
</template>
<template #main>
Main
</template>
</USplitter>
</div>
</template>
With nested splitters
Nest a Splitter inside a panel to build two-dimensional, IDE-style layouts.
<script setup lang="ts">
import type { SplitterItem } from '@nuxt/ui'
const card = 'bg-elevated/50 border border-default rounded-xl items-center justify-center text-muted font-medium'
const items: SplitterItem[] = [
{ slot: 'left', minSize: 20, class: card },
{ slot: 'right', minSize: 20 }
]
const nested: SplitterItem[] = [
{ slot: 'top', minSize: 20, class: card },
{ slot: 'bottom', minSize: 20, class: card }
]
</script>
<template>
<div class="w-full h-96">
<USplitter id="splitter-nested-example" :items="items">
<template #left>
Left
</template>
<template #right>
<USplitter id="splitter-nested-example-inner" orientation="vertical" :items="nested">
<template #top>
Top
</template>
<template #bottom>
Bottom
</template>
</USplitter>
</template>
</USplitter>
</div>
</template>
With custom handle
The handle is invisible by default. Use the ui prop to restyle it, for example as a visible divider for flush layouts, and the resize-handle slot to render content inside it like a grip.
<script setup lang="ts">
import type { SplitterItem } from '@nuxt/ui'
const items: SplitterItem[] = [
{ slot: 'left', minSize: 20, defaultSize: 30, class: 'items-center justify-center text-muted font-medium' },
{ slot: 'right', defaultSize: 70, class: 'items-center justify-center text-muted font-medium' }
]
</script>
<template>
<div class="w-full h-96">
<USplitter id="splitter-custom-handle-example" :items="items" :ui="{ handle: 'data-[orientation=horizontal]:w-px data-[orientation=vertical]:h-px bg-border transition-colors data-[state=hover]:bg-primary data-[state=drag]:bg-primary' }" class="rounded-lg border border-default overflow-hidden">
<template #left>
Left
</template>
<template #right>
Right
</template>
</USplitter>
</div>
</template>
With persistence
Provide an auto-save-id to persist the layout to localStorage and restore it on reload.
<template>
<USplitter id="my-layout" auto-save-id="my-layout" :items="items">
<!-- ... -->
</USplitter>
</template>
API
Props
| Prop | Default | Type |
|---|---|---|
as | 'div' | anyThe element or component this component should render as. |
id | stringA unique id for the group, also used to derive the ids of its panels and handles. Set it when rendering on the server, auto-generated ids can differ between the server and the client and break resizing on hydration. | |
orientation | 'horizontal' | "horizontal" | "vertical"The orientation of the splitter. |
items | T[] | |
disabled | false | boolean Whether the resize handles are disabled, locking the current layout. |
autoSaveId | null | stringUnique id used to auto-save group arrangement via | |
keyboardResizeBy | null | numberStep size when arrow key was pressed. | |
storage | PanelGroupStorageCustom storage API; defaults to localStorage | |
hitAreaMargins | PointerHitAreaMarginsAllow this much margin when determining resizable handle hit detection | |
ui | { root?: SlotClass; panel?: SlotClass; handle?: SlotClass; } |
Slots
| Slot | Type |
|---|---|
resize-handle | { index: number; ui: object; } |
Emits
| Event | Type |
|---|---|
layout | [val: number[]] |
collapse | [index: number] |
expand | [index: number] |
resize | [index: number, size: number, prevSize?: number | undefined] |
dragging | [index: number, dragging: boolean] |
Theme
export default defineAppConfig({
ui: {
splitter: {
slots: {
root: '',
panel: 'flex',
handle: 'group relative shrink-0 focus-visible:outline-2 focus-visible:outline-primary data-[panel-resize-handle-enabled=false]:cursor-default'
},
variants: {
orientation: {
horizontal: {
handle: 'w-2 cursor-col-resize'
},
vertical: {
handle: 'h-2 cursor-row-resize'
}
}
}
}
}
})
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
splitter: {
slots: {
root: '',
panel: 'flex',
handle: 'group relative shrink-0 focus-visible:outline-2 focus-visible:outline-primary data-[panel-resize-handle-enabled=false]:cursor-default'
},
variants: {
orientation: {
horizontal: {
handle: 'w-2 cursor-col-resize'
},
vertical: {
handle: 'h-2 cursor-row-resize'
}
}
}
}
}
})
]
})