Splitter

Panes whose proportions are adjusted by dragging a divider.

Contents

1. The Summer Door

2. Cicada Rain

3. Distant Thunder

Body

Drag the divider to resize the two sides.

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

Usage

import { Splitter, SplitterHandle, SplitterPanel } from '@hina-ui/vue'
ts

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.

Left panel

Right panel

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

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.

Top panel

Bottom panel

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

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.

Sidebar

15% – 40%

Main area

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

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.

Drag to the left edge to collapse

Main area

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

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.

Contents

Editor

Preview

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

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.

Resize, then reload

The width is kept

Main area

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

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.

Contents

Body

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

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 label overrides it. Name each divider when several are present.
  • The divider is not decoration. Do not remove it from the focus order with aria-hidden or a negative tabindex; 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