Split
<aparte-split>
Two panes and a draggable seam — a chat beside a preview, an editor, an artifact card.
The grid is the mechanism: --aparte-split-position is the primary pane’s size and
the min/max are CSS clamp bounds, so the browser clamps and nothing here parses a
unit. The element adds only what CSS cannot do — the drag, the arrow keys, the ARIA
of an APG window splitter, and one pane at a time under a breakpoint. Without the
element the .aparte-split recipe is still a split; it just does not move.
It stores nothing: position in, one aparte-split-resize out on commit. The
attribute is written on COMMIT only — a release, a key up, a double-click, a property
set — and the live value during a drag travels on the custom property, so a
framework’s reconciler is not in the drag loop. Persist from the event; restore by
setting position.
A pane CONTAINS a chat; a chat never contains a split.
aria-orientation on the handle is the INVERSE of this element’s orientation: the
attribute names the SEPARATOR’s own axis, which is what the APG means by “Left Arrow
moves a vertical splitter” and what ARIA 1.2 means by the attribute. Do not “fix” it —
consumers style against it.
Shift + an arrow is ±10%. That is an ecosystem convention, not the APG, which specifies the single step alone.
Example
Section titled “Example”<!-- The chat in one pane, your own in the other. Drag the seam, or tab to it and use the arrows; double-click resets it. `--aparte-split-min` is a CSS length, so the chat cannot be dragged narrower than 16rem whatever the percentage says. Under 48rem of window the split shows one pane, and the buttons switch it. --><div class="aparte-app-header"> <button class="aparte-btn aparte-btn--sm aparte-btn--surface" type="button" data-aparte-split-pane="start">Chat</button> <button class="aparte-btn aparte-btn--sm aparte-btn--surface" type="button" data-aparte-split-pane="end">Preview</button></div><aparte-split position="38" style="height: 22rem; --aparte-split-min: 16rem"> <aparte-chat> <aparte-chat-viewport> <aparte-chat-bubble message-id="u1" data-role="user" content="Show me the preview beside the chat."></aparte-chat-bubble> <aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant" content="It is the pane on the right. Drag the seam to give it more room, or double-click the seam to reset."></aparte-chat-bubble> </aparte-chat-viewport> <aparte-composer> <div class="aparte-composer-shell"> <div class="aparte-composer-row"> <aparte-composer-input></aparte-composer-input> <aparte-composer-send></aparte-composer-send> </div> </div> </aparte-composer> </aparte-chat> <section class="aparte-split__pane"> <!-- Your document, not ours: it brings its own colour scheme and typeface. --> <iframe title="Preview" style="inline-size: 100%; block-size: 100%; border: 0" srcdoc="<!doctype html><style>:root { color-scheme: light dark; font: 15px/1.5 system-ui, sans-serif; padding: 1rem 1.5rem } h1 { font-size: 1.25rem }</style><h1>Your pane</h1><p>A preview, a document, a map — anything that lives beside the conversation.</p>"></iframe> </section></aparte-split><script> document.querySelector('aparte-split').addEventListener('aparte-split-resize', (e) => { localStorage.setItem('split', String(e.detail.position)); });</script>Attributes
Section titled “Attributes”| Attribute | Description |
|---|---|
position | The primary pane’s size, as a percentage of the container. Reflected on commit, never per drag frame. |
orientation | horizontal (panes side by side, the default) or vertical (stacked). Names the CONTAINER’s axis. |
primary | start (default) or end: which pane position sizes. |
collapsed | The primary pane folded to its minimum. Enter on the seam toggles it; a second Enter restores the size it had. |
breakpoint | Below this width the split shows one pane at a time. A length (default 48rem), or none to never stack. |
pane | start (default) or end: which pane is shown while stacked. |
disabled | No drag, no keys, no tab stop; the seam stays drawn. |
label | The seam’s accessible name. Defaults to the locale’s splitHandleLabel. |
single | Show one pane — the one pane names — whatever the width: the seam and the other pane are gone, as under the breakpoint. For a host that decides itself when a second pane exists (a preview with nothing to preview yet). collapsed is not this: it folds the primary pane to --aparte-split-min and keeps the seam. The CSS route, .aparte-split--only-start / --only-end, is the same state for a host that owns its breakpoints. |
data-stacked | Written BY the element while one pane is shown. Read-only; style against it. |
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
position | number | The primary pane’s size, as a percentage of the container. Setting it commits. |
orientation | 'horizontal' | 'vertical' | The container’s axis: horizontal (side by side) or vertical (stacked). |
primary | 'start' | 'end' | Which pane position sizes. |
collapsed | boolean | The primary pane is folded to its minimum. |
pane | 'start' | 'end' | Which pane is shown while the split is stacked. |
disabled | boolean | No drag, no keys, no tab stop. The seam stays drawn. |
stacked (readonly) | boolean | True while the split is showing one pane. Read-only. Both routes into that state count. data-stacked is the one this element writes from its own breakpoint; .aparte-split--only-start / --only-end are the CSS route a host takes when it owns its breakpoints and sets breakpoint="none". The sheet gives the two byte-identical rules, so the element has to read them the same way — every guard downstream keys on this getter, and a split stacked by the class alone would measure a one-track grid and commit position="100". |
single | boolean | One pane on demand, whatever the width (#54). Reflected. |
Methods
Section titled “Methods”| Method | Description |
|---|---|
collapse(): void | Fold the primary pane to its minimum, remembering the size it had. |
expand(): void | Unfold it, back to the size it had before it collapsed. |
toggleCollapse(): void | |
showPane(which: 'start' | 'end'): void | Show one pane, which only has a visible effect while the split is stacked. |
reset(): void | Back to the position the split was FIRST connected with — a re-parent restores that, it does not redefine it. What a double-click on the seam does. |
Events
Section titled “Events”| Event | Type | Description |
|---|---|---|
aparte-split-resize | CustomEvent<AparteSplitResizeDetail> | The position settled: after a drag, a key, a double-click or a property set. Never during a drag — persist from here. |
Theming
Section titled “Theming”Override any of these on :root, on a subtree, or on one instance — custom properties inherit downward. Some are this element’s own; others are site-wide tokens that also style it, and overriding one of those at :root moves everything that reads it. The full set is in the CSS variables reference.
<aparte-split>
Section titled “<aparte-split>”| Variable | Default | Description |
|---|---|---|
--aparte-split-position | 38% | The primary pane’s size. |
--aparte-split-min | min(20rem, 100%) | Floor of the primary pane. Any length or percentage. |
--aparte-split-max | 60% | Ceiling of the primary pane. |
--aparte-split-handle-size | 12px | The seam’s track: the grab zone, and what the grid reserves between the panes. The painted line inside it is --aparte-split-seam-width. |
--aparte-split-hit-area | 12px | The invisible grab zone around it. Grows to the touch target on a coarse pointer. |
In a framework
Section titled “In a framework”The element is the same object everywhere — the tag does not change. What changes is how an attribute is written and how an event reaches you.
<aparte-split collapsed=""></aparte-split>el.addEventListener('aparte-split-resize', (e) => use(e.detail));<aparte-split collapsed=""></aparte-split>The aparte-* tags are typed JSX intrinsics as soon as you import from @aparte/react. A presence attribute takes '', never true — React stringifies it, and collapsed={false} would render collapsed="false", which hasAttribute reads as on. Events reach you by ref, typed through the DOM.
<template> <aparte-split collapsed="" @aparte-split-resize="(e) => use(e.detail)" ></aparte-split></template>Declared through Vue’s GlobalComponents, so vue-tsc checks the tag in any template. A presence attribute takes '' to set and null to remove, never false.
<aparte-split collapsed="" on:aparte-split-resize={(e) => use(e.detail)}></aparte-split>Declared through SvelteHTMLElements, so svelte-check covers the attributes and the on: handlers. A presence attribute takes '', never false.
import { AparteSplitDirective } from '@aparte/angular';<aparte-split [collapsed]="true" (splitResize)="use($event)"></aparte-split>A standalone directive whose selector IS the tag, so the real element sits in the template — @if, @for and content projection all reach it — and no CUSTOM_ELEMENTS_SCHEMA is needed.
Installation and the framework-specific traps: React · Vue · Svelte · Angular.