Skip to content

Splitter ​

A splitter divides a space into panels the reader can resize. It fills whatever contains it, so the container is what decides the overall size — a splitter with nothing around it has nothing to divide.

ts
import { Splitter, SplitterHandle, SplitterPanel } from "ropav";
Sidebar
Editor
orientation
template
<Splitter
  aria-label="Editor layout"
  class="h-48 w-full max-w-sm overflow-hidden rounded-xl border border-[var(--rp-border)]"
>
  <SplitterPanel class="grid h-full place-items-center text-sm" default-size="1fr" min-size="80px">
    Sidebar
  </SplitterPanel>
  <SplitterHandle />
  <SplitterPanel class="grid h-full place-items-center text-sm" default-size="2fr" min-size="80px">
    Editor
  </SplitterPanel>
</Splitter>

Panels and handles alternate, and there are as many handles as there are gaps. A horizontal splitter puts its panels side by side, which makes each handle a vertical line.

Sizing ​

default-size takes fr, px or %. A px panel holds its width when the container resizes and the fr beside it absorbs the difference, which is the shape of most sidebars. min-size and max-size clamp a drag and cannot be given in fr — a fraction has no fixed size to compare against.

is-collapsible changes what happens at the minimum: instead of stopping there, dragging past it snaps the panel shut. collapsed-size is what shut means — 0 takes the panel out of the layout, 48 leaves an icon rail.

Drag me shut
Editor
splitter-collapsible.vue
vue
<script setup lang="ts">
import { Splitter, SplitterHandle, SplitterPanel } from "ropav";
</script>

<template>
  <Splitter aria-label="Collapsible sidebar" class="frame">
    <SplitterPanel
      class="pane"
      :collapsed-size="48"
      default-size="200px"
      is-collapsible
      min-size="160px"
    >
      Drag me shut
    </SplitterPanel>

    <SplitterHandle show-grip />

    <SplitterPanel class="pane">Editor</SplitterPanel>
  </Splitter>
</template>

<style scoped>
.frame {
  border-color: var(--rp-border);
  height: calc(var(--rp-spacing) * 56);
  width: 100%;
  max-width: var(--rp-container-lg);
  overflow: hidden;
  border-radius: calc(var(--rp-radius) * 1.5);
  border-style: solid;
  border-width: 1px;
}

.pane {
  display: grid;
  height: 100%;
  place-items: center;
  font-size: var(--rp-text-sm);
  line-height: var(--rp-text-sm--line-height);
}
</style>

show-grip draws the mark across the middle of the divider. It is decoration: the grab area reaches well past the visible line either way, so turning it off makes the handle no harder to hit.

Remembering the layout ​

auto-save-id stores the sizes under ropav:splitter:<id> in localStorage and reads them back once after mount. A stored layout is discarded unless it still matches the panels on screen, which is why panels worth remembering should carry explicit ids — generated keys stop matching the moment a panel is added, removed or reordered.

template
<Splitter aria-label="Workspace" auto-save-id="workspace">
  <SplitterPanel id="sidebar" default-size="240px" min-size="160px">…</SplitterPanel>
  <SplitterHandle id="sidebar-handle" />
  <SplitterPanel id="editor">…</SplitterPanel>
</Splitter>

Controlled sizes ​

Pass sizes and a drag reports what it would do rather than doing it, leaving the layout to whatever you write back. default-sizes is the uncontrolled equivalent, and a panel can take size or default-size of its own instead.

Accessibility ​

  • Each handle is a role="separator" with the ARIA value attributes for the panel before it, so a reader hears a size rather than a divider.
  • Its aria-orientation is the inverse of the layout: a horizontal splitter has vertical dividers, and the attribute describes the divider.
  • Arrow keys along the axis move the handle by keyboard-step, and with Shift by keyboard-large-step. Home and End push it as far as it will go, Enter collapses or restores a collapsible neighbour, and Esc abandons a drag in progress.
  • Double-clicking a handle puts both its neighbours back to their declared default-size, and reopens a collapsed one on the way. That is a different gesture from Enter on purpose: one returns a panel to where the reader left it, the other to where the author put it.
  • Name the splitter with aria-label, and each handle with aria-label where "Resize panel" is not enough to tell two of them apart.

API ​

Splitter

PropTypeDefaultDescription
ariaLabelstring—Accessible name, for a splitter with no visible label.
ariaLabelledbystring—Ids of the elements that name the splitter.
autoSaveIdstring—Where the layout is remembered between visits, under ropav:splitter:<id> in localStorage. Read once after mount, and discarded unless it still matches the panels on screen.
defaultSizesFlexSize[]—Sizes the splitter starts at, when the caller is not controlling them.
isDisabledboolean—Suppresses every handle.
keyboardLargeStepnumber50How far a shift-arrow press moves a handle, in pixels.
keyboardStepnumber10How far one arrow press moves a handle, in pixels.
orientation"horizontal" | "vertical""horizontal"Axis the panels are laid out along. A horizontal group puts its panels side by side, which makes each handle a vertical line.
sizesFlexSize[]—Every panel's size, in order. Set, a drag reports what it would do rather than doing it.

The default slot receives:

Slot propType
isDisabledboolean
isDraggingboolean
layoutnumber[]
orientationSplitterOrientation
EventPayload
collapseCollectionKey
expandCollectionKey
resizeFlexSize[]
resizeEndFlexSize[]
resizeStartFlexSize[]
update:sizesFlexSize[]

SplitterHandle

PropTypeDefaultDescription
ariaLabelstring"Resize panel"Accessible name. Declared as ariaLabel, not aria-label: Vue normalises prop names, so a hyphenated declaration would never be matched.
ariaLabelledbystring—Ids of the elements that name the handle.
idCollectionKey—Identity of the handle. Falls back to a generated key.
isDisabledboolean—Disables this handle alone; the splitter being disabled also counts.
resetOnDoubleClickbooleantrueWhether a double-click puts both neighbours back to their default size.
showGripbooleanfalseDraws a grip across the middle of the divider, purely decorative — the grab area reaches well past it either way.

The default slot receives:

Slot propType
isDisabledboolean
isDraggingboolean
isFocusedboolean
isFocusVisibleboolean
isHoveredboolean
orientationSplitterOrientation

SplitterPanel

PropTypeDefaultDescription
collapsedSizeFlexSize0The size a collapsed panel keeps. 0 takes it out of the layout; 48 leaves an icon rail. Cannot be given in fr.
defaultSizeFlexSize"1fr"The size the panel starts at, and what a double-click on its handle restores.
idCollectionKey—Identity of the panel. Falls back to a generated key — but an explicit id is what lets a stored layout survive a panel being added, removed or reordered.
isCollapsibleboolean—Whether dragging past the minimum snaps the panel shut instead of stopping there.
maxSizeFlexSize—Largest size a drag may reach. Cannot be given in fr.
minSizeFlexSize0Smallest size a drag may reach. Cannot be given in fr.
sizeFlexSize—A size the caller controls. A drag never writes to it.

The default slot receives:

Slot propType
isCollapsedboolean
isCollapsibleboolean
orientationSplitterOrientation
sizenumber