Skip to content

Window Splitter

A window splitter lets users resize a primary pane with a focusable separator. <cinq-windowsplitter> is the layout wrapper (bounds + CSS custom properties). A nested [role="separator"] (or slider) is the interactive control: the component wires pointer drag, keyboard steps, collapse / restore, and layout updates (resize or clip).

Follows the WAI-ARIA Authoring Practices window splitter pattern. Inspired by @19h47/windowsplitter.

Terminal window
pnpm add @agencecinq/windowsplitter

Import once:

import "@agencecinq/windowsplitter";

Then wrap panes and a separator. The host owns layout context. ARIA lives on the separator.

HTML is the source of truth. The component will not invent missing attributes or warn about missing labels. Set role, tabindex, aria-orientation, value attributes, labelling, and aria-controls in HTML. Use an a11y linter (axe, Lighthouse) to catch invalid markup.

<cinq-windowsplitter class="panes">
<div id="toc" class="pane pane--primary" style="width: 30%">
<strong id="toc-label">Table of Contents</strong>
</div>
<div
role="separator"
tabindex="0"
aria-orientation="vertical"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="30"
aria-controls="toc"
aria-labelledby="toc-label"
></div>
<div class="pane pane--secondary">Content</div>
</cinq-windowsplitter>

The primary pane is resolved live via Element.ariaControlsElements on the separator.

aria-valuemin / aria-valuemax must match the real allowed range of the primary pane. If the pane cannot fully collapse, set min above 0.

Note on role: the APG documents role="separator". Some screen readers handle a focusable role="slider" more reliably. The component accepts either on the nested control. Pick what works for your AT testing.

Selector / attribute Required Role
<cinq-windowsplitter> Yes Layout wrapper / bounds container.
Nested [role="separator"] or [role="slider"] Yes Focusable control ($separator).
tabindex="0" Yes On the separator.
aria-orientation Yes On the separator: vertical or horizontal.
aria-valuemin / aria-valuemax / aria-valuenow Yes On the separator.
aria-controls Yes (for resize / clip) On the separator: id of the primary pane.
aria-label / aria-labelledby Yes On the separator.
Attribute / property Type Default Description
data-mode resize | clip | none resize How the primary pane is updated
data-step number 1 Arrow key increment
data-page number 10 PageUp / PageDown / Shift+Arrow
data-fixed boolean false Enter / pointer toggle only
disabled / aria-disabled boolean false Disable interaction (host)
formatSize (property) FormatSize `${ratio * 100}%` CSS size in resize mode
formatValue (property) FormatValue String(value) aria-valuetext
const host = document.querySelector("cinq-windowsplitter");
host.formatSize = ({ offset }) => `${offset}px`;
host.formatValue = (value) => `${value} %`;
Method Description
setValue(value, trigger?) Set aria-valuenow on the separator and update layout
collapse(trigger?) / restore(trigger?) / toggle(trigger?) Collapse helpers (history only from collapse / Enter)
sync() Re-read ARIA and re-apply layout
destroy() Remove listeners and observers. Called automatically from disconnectedCallback.

setValue / collapse / restore / toggle take an optional trigger flag (default true) to control whether windowsplitter:change is dispatched.

Property Description
$separator Focusable separator control
$primary Primary pane from ariaControlsElements (live)
value / min / max / ratio / orientation Read from separator ARIA
collapsed / disabled Derived state
mode / step / page / fixed Options
formatSize / formatValue Formatters
Hook When
[collapsed] Primary pane at aria-valuemin (host)
[dragging] Pointer drag in progress (host)
[disabled] / [aria-disabled="true"] Interaction disabled
:focus-visible on the separator Keyboard focus
--value / --ratio / --offset On the host (inherit to children for CSS-driven layouts)

The host also observes its own size via ResizeObserver and recalculates --offset when the container is resized.

Dispatched on the host <cinq-windowsplitter> (bubble). Prefer constants from @agencecinq/utils EVENTS:

Event Constant Detail When
windowsplitter:change WINDOWSPLITTER_CHANGE { value, min, max, ratio, collapsed } After a user-driven (or trigger: true) value change
import { EVENTS } from "@agencecinq/utils";
$host.addEventListener(EVENTS.WINDOWSPLITTER_CHANGE, ({ detail }) => {
console.log(detail.value, detail.collapsed);
});
Key Function
ArrowLeft / ArrowRight Move a vertical splitter
ArrowUp / ArrowDown Move a horizontal splitter
Enter Collapse / restore
Home / End Jump to min / max
PageUp / PageDown Step by page
Shift + Arrow Step by page

A fixed splitter (data-fixed) omits arrow keys and only implements Enter (and pointer toggle).

Dungeon map (vertical)

mode: 'resize': drag or arrow keys resize the primary pane.Enter collapses / restores.

Party roster

Primary pane, sized by the splitter value.

Encounter notes

Secondary pane fills the remaining space.

Spellbook (horizontal)

aria-orientation="horizontal": Up / Down move the bar;Enter collapses the top pane.

Prepared spells

Primary pane. Height follows the splitter value.

Spell description

Secondary pane fills the remaining space below.

Before / after (clip)

data-mode="clip": overlay panes and clip the primary layer (same interaction as a comparison slider).

Before