Skip to content

Splitter

hc-splitter splits two panes with a movable handle — the shadcn Resizable equivalent. It follows the WAI-ARIA Window Splitter pattern: the handle is a focusable role="separator" whose aria-valuenow tracks the primary pane’s size. installSplitter wires the pointer drag, keyboard resize, and ARIA; the layout is plain flexbox driven by a single --hc-splitter-pos custom property.

Also known as: split pane, resizable panes.

Give the container a cross-axis size (a height for the default horizontal layout).

Primary pane
Secondary pane
import { installSplitter } from '@hypermedia-components/core';
installSplitter(); // idempotent; returns an uninstaller

The zero-config @hypermedia-components/core/behaviors entry installs it automatically and picks up content swapped in by htmx.

data-orientation="vertical" stacks the panes with a horizontal handle.

Top
Bottom
AttributePurpose
data-orientationhorizontal (default, side-by-side) or vertical (stacked).
data-valueInitial primary-pane size, percent (default 50).
data-min / data-maxAllowed range, percent (default 10 / 90).
data-stepKeyboard step, percent (default 5).
data-collapsibleDouble-click / Enter on the handle toggles the primary pane collapsed ↔ last-open.
data-persistlocalStorage key — saves and restores the position (a persisted 0 restores collapsed).

Tab to the handle, then:

KeyAction
/ (horizontal) or / (vertical)Resize by one step
HomeMinimum size
EndMaximum size
EnterToggle collapse (when data-collapsible)

DOM focus is on the handle; aria-valuenow / aria-valuemin / aria-valuemax describe the primary pane’s size to assistive tech.

Each resize dispatches a bubbling hc:splitterchange on the container:

splitter.addEventListener('hc:splitterchange', (e) => {
const { value, collapsed, orientation } = e.detail; // value: primary-pane %
});

collapsed is true while the primary pane is folded away (see below).

Add data-collapsible so the handle can fold the primary pane away and bring it back. Double-click the handle, or focus it and press Enter, to toggle between collapsed (0%) and the last open size.

Primary pane — double-click the handle
Secondary pane

A collapsed splitter carries data-collapsed on the container — style the panes off it — and hc:splitterchange reports detail.collapsed. The handle’s aria-valuenow stays within [min, max]; the collapsed state rides on data-collapsed rather than an out-of-range value.

Add data-persist="<key>" to mirror the position into localStorage and restore it on the next visit. A persisted 0 restores the collapsed state. Storage failures (private mode, disabled) degrade silently to the data-value default.

<div class="hc-splitter" data-orientation="horizontal"
data-collapsible data-persist="editor:sidebar" style="block-size:10rem">
</div>

data-persist covers the common localStorage case; reach for Hyperscript when you need custom storage (a cookie, a server round-trip, a different key shape):

<div class="hc-splitter" data-orientation="horizontal"
_="on hc:splitterchange call myStore.set('split', event.detail.value)">
</div>

More patterns: Hyperscript → Reacting to component events.

A pane can collapse to its own content — a rail holding the control that brings it back:

<div class="hc-splitter__panel" data-collapsed>
<button class="hc-button" data-size="sm" type="button">Reasons (5)</button>
</div>

data-collapsed gives the pane its content’s size, hands the freed space to its sibling, and hides the drag handle (there is nothing to drag). The state belongs to whoever owns the pane’s content, so a server re-rendering that region can set it on the fragment it already sends — no client state, and the two panes cannot disagree.

Collapse to a rail, not to nothing. A panel that disappears when closed is a dead end: the user who closed it has no way to find it again.

  • The handle is role="separator" + tabindex="0" with aria-valuenow / aria-valuemin / aria-valuemax and aria-controls pointing at the primary pane (per the APG Window Splitter pattern).
  • The separator’s aria-orientation is set automatically (a side-by-side split uses a vertical separator line, and vice-versa).
  • Give the handle an accessible name with aria-label or aria-labelledby (the primary pane’s heading).
Token pathPurpose
splitter.handle-sizeHandle thickness (the hit area).
splitter.handle-bg / handle-hover-bgHandle background (hover / focus tints with data-color).
splitter.grip-color / grip-active-colorThe centred grip bar.
splitter.grip-length / grip-thicknessGrip dimensions.
Show the generated CSS variables
--hc-splitter-handle-size | -handle-bg | -handle-hover-bg
--hc-splitter-grip-color | -grip-active-color | -grip-length | -grip-thickness
--hc-splitter-pos (runtime: the primary pane's size, set by installSplitter)

Three-or-more panes, nested splitters, and pixel-based minimums are deferred.

  • Separator — a static, non-interactive divider.
  • Card — common content for the panes.