<script setup lang="ts">
import { Card, Splitter, SplitterHandle, SplitterPanel, Stack, Text } from '@hina-ui/vue'
const chapters = ['1. The Summer Door', '2. Cicada Rain', '3. Distant Thunder']
</script>
<template>
<Card :padded="false" class="w-full max-w-2xl overflow-hidden">
<Splitter class="h-64">
<SplitterPanel :default-size="32" :min-size="20">
<Stack gap="xs" class="p-4">
<Text size="sm" tone="faint">Contents</Text>
<Text v-for="chapter in chapters" :key="chapter" size="sm">{{ chapter }}</Text>
</Stack>
</SplitterPanel>
<SplitterHandle />
<SplitterPanel :default-size="68">
<Stack gap="sm" class="p-4">
<Text size="sm" tone="faint">Body</Text>
<Text size="sm">Drag the divider to resize the two sides.</Text>
</Stack>
</SplitterPanel>
</Splitter>
</Card>
</template>
Usage
import { Splitter, SplitterHandle, SplitterPanel } from '@hina-ui/vue'
Splitter is the container, SplitterPanel is a pane and SplitterHandle is the draggable divider between panes. Panes and dividers alternate: one divider sits between every two panes.
The container needs a definite size. A horizontal splitter needs a height; a vertical one needs both height and width. Sizes are given to Splitter through class. Without a height the panes collapse to nothing and the splitter looks as though it never rendered.
<script setup lang="ts">
import { Center, Splitter, SplitterHandle, SplitterPanel, Text } from '@hina-ui/vue'
</script>
<template>
<Splitter class="border-line h-48 w-full max-w-xl rounded-lg border">
<SplitterPanel>
<Center class="h-full">
<Text size="sm" tone="muted">Left panel</Text>
</Center>
</SplitterPanel>
<SplitterHandle />
<SplitterPanel>
<Center class="h-full">
<Text size="sm" tone="muted">Right panel</Text>
</Center>
</SplitterPanel>
</Splitter>
</template>
Pane sizes are always percentages between 0 and 100. Pixel values are not accepted.
Examples
Direction
direction sets the axis, defaulting to horizontal for side-by-side panes; vertical stacks them. The divider’s orientation, its drag axis and the arrow keys that move it all follow, with nothing else to configure.
<script setup lang="ts">
import { Center, Splitter, SplitterHandle, SplitterPanel, Text } from '@hina-ui/vue'
</script>
<template>
<Splitter direction="vertical" class="border-line h-64 w-full max-w-xl rounded-lg border">
<SplitterPanel :default-size="40">
<Center class="h-full">
<Text size="sm" tone="muted">Top panel</Text>
</Center>
</SplitterPanel>
<SplitterHandle />
<SplitterPanel :default-size="60">
<Center class="h-full">
<Text size="sm" tone="muted">Bottom panel</Text>
</Center>
</SplitterPanel>
</Splitter>
</template>
Size constraints
defaultSize is a pane’s initial share; minSize and maxSize bound how far it can be dragged. All three are percentages. Panes without a defaultSize share the remaining space evenly.
Constraints keep a pane from being dragged to an unusable width — a contents pane too narrow to read the entries, for instance.
Within one group, either give every pane a defaultSize or give none of them one. The even share is worked out in the browser; on the server the container size is unknown, so an unsized pane falls back to an equal-share baseline. If a neighbouring pane does carry a defaultSize, the server-rendered first paint is visibly out of proportion and only snaps to the right ratio once the browser takes over. With no sizes at all the panes come out even, and server and browser agree.
<script setup lang="ts">
import { Center, Splitter, SplitterHandle, SplitterPanel, Stack, Text } from '@hina-ui/vue'
</script>
<template>
<Splitter class="border-line h-48 w-full max-w-xl rounded-lg border">
<SplitterPanel :default-size="25" :min-size="15" :max-size="40">
<Center class="h-full px-3">
<Stack gap="none" align="center">
<Text size="sm" tone="muted">Sidebar</Text>
<Text size="sm" tone="faint">15% – 40%</Text>
</Stack>
</Center>
</SplitterPanel>
<SplitterHandle />
<SplitterPanel :default-size="75">
<Center class="h-full">
<Text size="sm" tone="muted">Main area</Text>
</Center>
</SplitterPanel>
</Splitter>
</template>
Collapsible
collapsible lets a pane be dragged to collapsedSize and stay there instead of springing back to minSize. collapsedSize defaults to 0, which collapses the pane entirely.
This is not the same as minSize: minSize is the floor during a drag, while collapsible snaps to the collapsed state once that floor is crossed.
<script setup lang="ts">
import { Center, Splitter, SplitterHandle, SplitterPanel, Text } from '@hina-ui/vue'
</script>
<template>
<Splitter class="border-line h-48 w-full max-w-xl rounded-lg border">
<SplitterPanel collapsible :collapsed-size="0" :default-size="28" :min-size="18">
<Center class="h-full px-3">
<Text size="sm" tone="muted">Drag to the left edge to collapse</Text>
</Center>
</SplitterPanel>
<SplitterHandle />
<SplitterPanel :default-size="72">
<Center class="h-full">
<Text size="sm" tone="muted">Main area</Text>
</Center>
</SplitterPanel>
</Splitter>
</template>
Nesting
A pane can hold another Splitter, usually running along the opposite axis, which gives layouts such as contents plus editor plus preview. Give the inner container h-full so it fills the pane it sits in.
<script setup lang="ts">
import { Center, Splitter, SplitterHandle, SplitterPanel, Text } from '@hina-ui/vue'
</script>
<template>
<Splitter class="border-line h-64 w-full max-w-xl rounded-lg border">
<SplitterPanel :default-size="30" :min-size="20">
<Center class="h-full">
<Text size="sm" tone="muted">Contents</Text>
</Center>
</SplitterPanel>
<SplitterHandle />
<SplitterPanel :default-size="70">
<Splitter direction="vertical" class="h-full">
<SplitterPanel :default-size="65">
<Center class="h-full">
<Text size="sm" tone="muted">Editor</Text>
</Center>
</SplitterPanel>
<SplitterHandle />
<SplitterPanel :default-size="35">
<Center class="h-full">
<Text size="sm" tone="muted">Preview</Text>
</Center>
</SplitterPanel>
</Splitter>
</SplitterPanel>
</Splitter>
</template>
Remembering sizes
autoSaveId stores the current proportions in browser storage and restores them on the next visit. Several splitters on one page need distinct ids, otherwise they overwrite one another.
Once shipped, an id should not change: changing it discards the proportions people have saved and falls back to the defaults.
<script setup lang="ts">
import { Center, Splitter, SplitterHandle, SplitterPanel, Stack, Text } from '@hina-ui/vue'
</script>
<template>
<Splitter
auto-save-id="hina-docs-splitter-demo"
class="border-line h-48 w-full max-w-xl rounded-lg border"
>
<SplitterPanel :default-size="35" :min-size="20">
<Center class="h-full px-3">
<Stack gap="none" align="center">
<Text size="sm" tone="muted">Resize, then reload</Text>
<Text size="sm" tone="faint">The width is kept</Text>
</Stack>
</Center>
</SplitterPanel>
<SplitterHandle />
<SplitterPanel :default-size="65">
<Center class="h-full">
<Text size="sm" tone="muted">Main area</Text>
</Center>
</SplitterPanel>
</Splitter>
</template>
Divider name
The divider carries an accessible name in the interface language. When a page holds several dividers and the default name cannot tell them apart, name each one with label.
<script setup lang="ts">
import { Center, Splitter, SplitterHandle, SplitterPanel, Text } from '@hina-ui/vue'
</script>
<template>
<Splitter class="border-line h-48 w-full max-w-xl rounded-lg border">
<SplitterPanel :default-size="35">
<Center class="h-full">
<Text size="sm" tone="muted">Contents</Text>
</Center>
</SplitterPanel>
<SplitterHandle label="Resize the contents pane" />
<SplitterPanel :default-size="65">
<Center class="h-full">
<Text size="sm" tone="muted">Body</Text>
</Center>
</SplitterPanel>
</Splitter>
</template>
Accessibility
- The divider is focusable and shows a focus ring; arrow keys adjust the proportions by a fixed step, equivalent to dragging.
- The divider’s role and current proportion come from the underlying primitive, so screen readers announce its name along with the current percentage.
- The default accessible name follows the interface language (“Resize panel” in English) and
labeloverrides it. Name each divider when several are present. - The divider is not decoration. Do not remove it from the focus order with
aria-hiddenor a negativetabindex; that leaves keyboard users unable to adjust the panes.
API
Splitter
| Prop | Type | Default | Description |
|---|---|---|---|
direction | 'horizontal' | 'vertical' | 'horizontal' | Axis the panes are laid out along |
autoSaveId | string | — | Id under which proportions are saved |
class | string | — | Classes appended to the root |
| Slot | Description |
|---|---|
default | Alternating panes and dividers |
SplitterPanel
| Prop | Type | Default | Description |
|---|---|---|---|
defaultSize | number | — | Initial share, as a percentage |
minSize | number | — | Lower bound while dragging, as a percentage |
maxSize | number | 100 | Upper bound while dragging, as a percentage |
collapsible | boolean | false | Whether it snaps shut past the lower bound |
collapsedSize | number | 0 | Share when collapsed, as a percentage |
class | string | — | Classes appended to the root |
| Slot | Description |
|---|---|
default | Pane content |
SplitterHandle
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | Interface language | Accessible name of the divider |
class | string | — | Classes appended to the root |