<!-- Press the seam and move it, or tab to it and use the arrow keys — Shift for ten
percent at a time, Home and End for the extremes, Enter to collapse, double-click to
reset. `--aparte-split-min` is a CSS length, so the chat cannot be dragged narrower
than 16rem whatever the percentage says: the browser clamps, nothing in JS parses a
unit.
The split stores nothing. `position` goes in, one `aparte-split-resize` comes out
on release — persist from there.
Under its breakpoint (30rem here, narrower than the 48rem default so this frame shows
both panes; restated in the style rule so the buttons agree) the split shows ONE pane: set the width to 375 above and the two buttons switch
it through `data-aparte-split-pane` and no script at all.
The buttons are hidden above that same width, which is the rule rather than the
detail: above the breakpoint there is only one layout, so a control that switches
panes has nothing to switch and would read as broken. Hide it with the media query
the split already answers to. -->
<style>
#split-panes { display: none }
@media (max-width: 30rem) {
#split-panes { display: flex; gap: var(--aparte-space-2); margin-block-end: var(--aparte-space-3) }
}
</style>
<div id="split-panes">
<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" breakpoint="30rem" style="height: 22rem; --aparte-split-min: 16rem; --aparte-split-max: 65%">
<aparte-chat>
<aparte-chat-viewport>
<aparte-chat-bubble message-id="u1" data-role="user" content="Make the hero headline bigger and pin the header."></aparte-chat-bubble>
<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant" content="Done — the headline is 20px now and the header stays at the top while the page scrolls."></aparte-chat-bubble>
<aparte-chat-bubble message-id="u2" data-role="user" content="Give me more room for the preview."></aparte-chat-bubble>
<aparte-chat-bubble message-id="a2" data-role="assistant" name="Assistant" content="That one is yours: drag the seam. I cannot reach past 65% either — the max is a CSS bound on the pane, not a rule I follow."></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">
<iframe title="Preview" style="inline-size: 100%; block-size: 100%; border: 0"
srcdoc="<body style='margin:0; padding:28px; font:15px/1.6 system-ui, sans-serif; color:#1b1b1f; background:#fbf9f5'><h1 style='margin:0 0 10px; font-size:20px'>Your pane</h1><p style='margin:0; color:#6b6b76'>A preview frame, an editor, a canvas — whatever the chat is building. It is a real iframe, and the drag keeps tracking across it.</p></body>"></iframe>
</section>
</aparte-split>When the chat builds something, the result needs room beside it. <aparte-split> puts two panes
side by side with a seam between them: drag it, or focus it and use the arrow keys. A pane holds a
chat; a chat never holds a split.
The size is CSS: --aparte-split-position is the first pane’s size, and --aparte-split-min and
--aparte-split-max bound it in any unit, so the browser does the clamping. Without the element,
the .aparte-split classes give the same two panes with no drag.
Variants
Section titled “Variants”Stacked
Section titled “Stacked”<!-- orientation="vertical": the chat above, the pane below, the seam dragged up and down. -->
<aparte-split orientation="vertical" position="62" breakpoint="none" style="height: 24rem">
<aparte-chat>
<aparte-chat-viewport>
<aparte-chat-bubble message-id="u1" data-role="user" content="Run the tests."></aparte-chat-bubble>
<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant" content="Running them now; the output is in the pane below."></aparte-chat-bubble>
</aparte-chat-viewport>
</aparte-chat>
<section class="aparte-split__pane">
<!-- The pane is yours: here, a plain log. -->
<pre tabindex="0" aria-label="Test output" style="margin: 0; padding: 1.25rem; font: 13px/1.6 var(--aparte-code-font-family)">✓ build
✓ unit tests
✓ browser tests</pre>
</section>
</aparte-split>orientation="vertical" puts the panes one above the other. primary="end" makes the second pane
the one position sizes.
<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.
The split stores nothing: position goes in, and aparte-split-resize comes out once the reader
lets go. Save the value from the event, and restore it by setting position.
On a narrow window (under breakpoint, 48rem by default, or none), the split shows one pane at a
time; buttons carrying data-aparte-split-pane="start" or "end" switch between them, with no
script. single shows one pane at any width, and collapsed folds the first pane to its minimum.
Customize
Section titled “Customize”| Variable | What it sets |
|---|---|
--aparte-split-position | The first pane’s size (38%). |
--aparte-split-min, --aparte-split-max | Its bounds, in any unit. |
--aparte-split-handle-size | The seam’s width. |
<aparte-split> · class AparteSplit
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
| 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
| 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
| 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. |
CSS variables
| 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. |
Classes
Section titled “Classes”.aparte-split · .aparte-split--only-end · .aparte-split--only-start · .aparte-split--primary-end · .aparte-split--vertical · .aparte-split__handle · .aparte-split__pane · .aparte-split__scrim
Accessibility
Section titled “Accessibility”- The seam is a separator with its current value, named from the
splitHandleLabelkey (orlabel). It follows the window splitter pattern: arrow keys move it, Shift moves it by 10%, Home and End go to the bounds, Enter folds the first pane. - Its grab zone grows to a touch-sized target on a coarse pointer.
Related
Section titled “Related”- App shell — the main area a split fills.
- Artifact — a document that belongs in the second pane.
- Layout guide — the builder layout, step by step.