Skip to content

CSS classes

Core is light DOM: it writes ordinary classes onto ordinary elements, and so can you. The classes below are the ones aparté styles for its own use and leaves available — put aparte-btn on a button of yours and it looks like the send button, with no component to mount and no framework involved.

Your own class stays on the element for events and for targeting. It just stops carrying the look.

Everything here is themed by the CSS variables; the recipes read those tokens, so a rebrand reaches your buttons and aparté’s at the same time.

Things a person operates. Each is one base class plus modifiers that compose — an intent says what the control means, a fill says how loudly to say it, and the two axes do not know about each other.

aparté — the button. Ready-made classes; nothing to configure.

TWO AXES, and keeping them apart is the whole design. An INTENT says which colour the button means; a FILL says what to do with that colour. Seven intents times five fills is thirty-five buttons out of twelve classes, and every combination works because neither axis knows about the other.

An intent on its own is a ghost — the quietest form, and the one a chat uses most. Add a fill when the button has to carry more weight.

.aparte-btn-group fuses adjacent buttons into ONE control: the inner corners drop, the shared borders overlap, and only the group’s ends keep the radius. Give the wrapper role="group" and a name — visually one control, it should read as one too. A SEGMENTED control is the same group with one segment selected: mark it aria-pressed="true" (or aria-selected="true" under role="tablist") and it is painted solid in the group’s intent, at rest — not only under the pointer.

This layer is NEUTRAL: it is what a UI library offers, not a summary of what this repo happens to use. A variant nothing wears still ships, because the plugin that needs it is not written yet, and its absence is what makes an author invent a seventh shade of orange.

Your own class stays on the element, for events and for targeting. It just stops carrying the look.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<button class="aparte-btn aparte-btn--primary aparte-btn--solid">Send</button>
<button class="aparte-btn aparte-btn--danger aparte-btn--outline">Delete</button>
<button class="aparte-btn aparte-btn--icon" aria-label="Copy">
<aparte-icon name="copy"></aparte-icon>
</button>
<div class="aparte-btn-group" role="group" aria-label="Versions">
<button class="aparte-btn aparte-btn--surface" aria-label="Previous"><aparte-icon name="prevBranch"></aparte-icon></button>
<button class="aparte-btn aparte-btn--surface">1 / 2</button>
<button class="aparte-btn aparte-btn--surface" aria-label="Next"><aparte-icon name="nextBranch"></aparte-icon></button>
</div>
<div class="aparte-btn-group" role="group" aria-label="View">
<button class="aparte-btn aparte-btn--primary aparte-btn--outline" aria-pressed="true">Chat</button>
<button class="aparte-btn aparte-btn--primary aparte-btn--outline" aria-pressed="false">Preview</button>
</div>

.aparte-btn · .aparte-btn--block · .aparte-btn--circle · .aparte-btn--danger · .aparte-btn--ghost · .aparte-btn--icon · .aparte-btn--info · .aparte-btn--lg · .aparte-btn--neutral · .aparte-btn--outline · .aparte-btn--pill · .aparte-btn--primary · .aparte-btn--secondary · .aparte-btn--sm · .aparte-btn--soft · .aparte-btn--solid · .aparte-btn--success · .aparte-btn--surface · .aparte-btn--warning · .aparte-btn--xl · .aparte-btn-group · .aparte-btn__icon · .aparte-icon

aparté — form fields. Ready-made classes; nothing to configure.

Every input control a UI library offers: text entry, textarea, select, checkbox, radio, switch, range. The library renders into the LIGHT DOM — there is no shadow root and no ::part() — so a class is the only handle a consumer has onto a native form control, and appearance: none is how this file takes the browser’s own chrome off a checkbox/radio/switch/range so these classes can repaint it.

.aparte-field is ONE shared recipe — border, background, radius, padding, focus ring, placeholder — applied to <input>, <textarea> and <select> alike, so a form built from all three still reads as one control family. Checkbox, radio and switch are drawn from the native control itself (appearance: none plus a pseudo-element), never a div standing in for one, so the browser’s own focus/keyboard/AT handling stays intact — only the paint changes.

Checkbox, radio, switch and range carry an INTENT axis, exactly like the button: a checked checkbox can be success or danger, not only primary. Unlike the button there is no separate FILL axis here — a checkbox has exactly one filled look, not five — so intent is the only modifier each of these needs.

This layer is NEUTRAL: it is what a UI library offers, not a summary of what this repo happens to use. A control nothing wears yet still ships, because the form the repo hasn’t built is not a reason to leave it out.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<label class="aparte-field-label" for="email">
Email <span class="aparte-field-required" aria-hidden="true">*</span>
</label>
<input id="email" class="aparte-field" type="email" placeholder="you@example.com" />
<p class="aparte-field-hint">We only use this to send a receipt.</p>
<p class="aparte-field-warning">Changing it signs you out of other devices.</p>
<div class="aparte-field-group">
<span class="aparte-field-group__prefix">https://</span>
<input class="aparte-field" placeholder="example.com" />
</div>
<p><label><input type="checkbox" class="aparte-checkbox aparte-checkbox--danger" /> Delete my account</label></p>
<p><label><input type="checkbox" role="switch" class="aparte-switch aparte-switch--success" checked /> Email notifications</label></p>

.aparte-btn · .aparte-checkbox · .aparte-checkbox--danger · .aparte-checkbox--info · .aparte-checkbox--neutral · .aparte-checkbox--primary · .aparte-checkbox--secondary · .aparte-checkbox--success · .aparte-checkbox--warning · .aparte-color · .aparte-field · .aparte-field--lg · .aparte-field--sm · .aparte-field--textarea · .aparte-field-choice · .aparte-field-choice--boxed · .aparte-field-choice--selected · .aparte-field-choice__body · .aparte-field-error · .aparte-field-group · .aparte-field-group__prefix · .aparte-field-group__suffix · .aparte-field-hint · .aparte-field-label · .aparte-field-required · .aparte-field-row · .aparte-field-warning · .aparte-fieldset · .aparte-meter · .aparte-output · .aparte-radio · .aparte-radio--danger · .aparte-radio--info · .aparte-radio--neutral · .aparte-radio--primary · .aparte-radio--secondary · .aparte-radio--success · .aparte-radio--warning · .aparte-range · .aparte-range--danger · .aparte-range--info · .aparte-range--neutral · .aparte-range--primary · .aparte-range--secondary · .aparte-range--success · .aparte-range--warning · .aparte-switch · .aparte-switch--danger · .aparte-switch--info · .aparte-switch--neutral · .aparte-switch--primary · .aparte-switch--secondary · .aparte-switch--success · .aparte-switch--warning

aparté — Display primitives: the non-interactive pieces a UI library offers.

Ready-made classes; nothing to configure.

This layer is NEUTRAL: it is what a UI library offers, not a summary of what this repo happens to use. A class nothing wears today still ships, because the plugin that needs it is not written yet.

Badge and alert repeat the button’s two-axis shape — an intent class sets a local custom property, a fill (or the intent alone) reads it — for the same reason: colour meaning and paint style are independent questions, and collapsing them into one flat list of classes is what produces a ninth accidental shade of orange.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<span class="aparte-badge aparte-badge--success aparte-badge--solid">Live</span>
<div class="aparte-progress" style="--aparte-progress-value: 60">
<div class="aparte-progress__bar"></div>
</div>
<div class="aparte-alert aparte-alert--warning" role="alert">
<span class="aparte-alert__icon"><aparte-icon name="alertTriangle"></aparte-icon></span>
<div class="aparte-alert__body">
<p class="aparte-alert__title">Heads up</p>
<p class="aparte-alert__message">Your session expires soon.</p>
</div>
</div>

aparte-avatar — sizes, shape, image + initials fallback

--aparte-avatar-size is NOT invented here: it already exists centrally for the message avatar (bubble.css), and this component reads the same one — a generic avatar and a chat-message avatar are the same object, and a size modifier applied here resizes a role-coloured message avatar too. The corner, the initials and the group’s overlap are FRACTIONS of that size (--aparte-avatar-radius-ratio, --aparte-avatar-initials-ratio, --aparte-avatar-overlap-ratio), computed on the element so a size modifier moves them: as absolute tokens the 40px avatar drew the same 11px initials as the 32px one, the corner drifted from squircle to square up the ramp, and a 6px overlap was a fifth of a small avatar and a tenth of a large one.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<span class="aparte-avatar aparte-avatar--sm">AP</span>
<span class="aparte-avatar">JD</span>
<span class="aparte-avatar aparte-avatar--lg aparte-avatar--square">MK</span>
<span class="aparte-avatar-group">
<span class="aparte-avatar">AP</span>
<span class="aparte-avatar">JD</span>
<span class="aparte-avatar">+3</span>
</span>

.aparte-avatar · .aparte-avatar--circle · .aparte-avatar--lg · .aparte-avatar--sm · .aparte-avatar--square · .aparte-avatar--xl · .aparte-avatar--xs · .aparte-avatar-group · .aparte-avatar__image

aparté — the icon. Every glyph the library draws carries this class.

The glyph does NOT carry its own size. It used to — width="14", width="16", width="20" baked into the markup — and that is precisely what kept the same drawing from being shared: an ✕ sized for a remove button could not also be the ✕ of a tool call, so the library ended up with three of them, on three grids, at three stroke widths. Size is a property of the PLACE an icon sits in, so it is declared by the container and inherited down.

--aparte-icon-size is therefore the one knob, and it inherits: set it on any ancestor and every glyph below follows. A container with a rule of its own (.aparte-action-button svg, .aparte-tool-icon svg) still wins on specificity — those already expressed size in CSS and are left alone.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<aparte-icon name="check"></aparte-icon>

.aparte-icon · .aparte-icon--lg · .aparte-icon--sm · .aparte-icon--xl · .aparte-icon-spin

aparte-badge — small count or status pill

Two axes like the button’s: an intent says what it means, a fill says how loudly. --dot is the third form — no label, so it is always painted solid.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<span class="aparte-badge aparte-badge--success aparte-badge--solid">Live</span>
<span class="aparte-badge aparte-badge--warning">Draft</span>
<span class="aparte-badge aparte-badge--danger aparte-badge--outline">Failed</span>
<span class="aparte-badge aparte-badge--info aparte-badge--sm">12</span>
<span class="aparte-badge aparte-badge--danger aparte-badge--dot" aria-label="Unread"></span>

.aparte-badge · .aparte-badge--danger · .aparte-badge--dot · .aparte-badge--info · .aparte-badge--lg · .aparte-badge--neutral · .aparte-badge--outline · .aparte-badge--primary · .aparte-badge--secondary · .aparte-badge--sm · .aparte-badge--soft · .aparte-badge--solid · .aparte-badge--success · .aparte-badge--warning

aparte-tag — a removable chip

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<span class="aparte-tag">
<span class="aparte-tag__label">typescript</span>
<button class="aparte-tag__remove" aria-label="Remove typescript"><aparte-icon name="close"></aparte-icon></button>
</span>
<span class="aparte-tag aparte-tag--sm"><span class="aparte-tag__label">draft</span></span>

.aparte-tag · .aparte-tag--sm · .aparte-tag__label · .aparte-tag__remove

aparté — the thumbnail. A media tile: a fixed square that clips whatever is dropped

into it, with room for a control in a corner.

Distinct from .aparte-avatar, which is the same shape for a different reason: an avatar stands for a PERSON and falls back to initials, so it carries type styling and a text colour. A thumbnail stands for a FILE and falls back to an icon or a label, so it carries none of that — and it is position: relative because a remove button or a badge sits on it, which an avatar never has.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<span class="aparte-thumbnail aparte-thumbnail--lg"><img class="aparte-thumbnail__image"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2 3'%3E%3Crect width='2' height='3' fill='%23b07d33'/%3E%3C/svg%3E"
alt="A screenshot"></span>
<span class="aparte-thumbnail"><span class="aparte-thumbnail__label">PDF</span></span>
<span class="aparte-thumbnail aparte-thumbnail--sm"><span class="aparte-thumbnail__label">MD</span></span>

.aparte-attachments · .aparte-thumb · .aparte-thumb--file · .aparte-thumb--image · .aparte-thumb__name · .aparte-thumbnail · .aparte-thumbnail--lg · .aparte-thumbnail--sm · .aparte-thumbnail__image · .aparte-thumbnail__label

aparte-spinner — a rotating indicator

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<span class="aparte-spinner aparte-spinner--sm" role="status" aria-label="Loading"></span>
<span class="aparte-spinner" role="status" aria-label="Loading"></span>
<span class="aparte-spinner aparte-spinner--lg" role="status" aria-label="Loading"></span>

.aparte-spinner · .aparte-spinner--lg · .aparte-spinner--sm

aparte-progress — determinate bar + indeterminate variant

The consumer owns the ARIA: role=“progressbar” plus aria-valuenow/min/max on the determinate form, and aria-valuetext (or nothing, per platform convention) on the indeterminate one — this file only paints what those attributes mean.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<div class="aparte-progress" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100"
style="--aparte-progress-value: 60">
<div class="aparte-progress__bar"></div>
</div>
<div class="aparte-progress aparte-progress--indeterminate" role="progressbar" aria-label="Working">
<div class="aparte-progress__bar"></div>
</div>

.aparte-progress · .aparte-progress--indeterminate · .aparte-progress__bar

aparte-skeleton — a loading placeholder

Decorative: give the element aria-hidden=“true” (or wrap the group behind a single aria-busy region) so a screen reader does not read out an empty box. A skeleton is a block with no margin of its own: the recipe spaces consecutive text lines and nothing else, so a GROUP is laid out by its container — a grid with a gap, the way the real card it stands in for is. Four lines glued edge to edge is what the example used to show.

The last line is shorter because the last line of a paragraph is, and the block sits in the text column, under the lines it stands for — not across the avatar’s. The rect’s height is the family’s token (--aparte-skeleton-rect-height), not an inline value that contradicts it.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<div style="display: grid; grid-template-columns: auto 1fr; gap: var(--aparte-space-3); align-items: start" aria-busy="true">
<span class="aparte-skeleton aparte-skeleton--circle" aria-hidden="true"></span>
<div>
<span class="aparte-skeleton aparte-skeleton--text" aria-hidden="true"></span>
<span class="aparte-skeleton aparte-skeleton--text" aria-hidden="true"></span>
<span class="aparte-skeleton aparte-skeleton--text" style="max-inline-size: 60%" aria-hidden="true"></span>
</div>
<span class="aparte-skeleton aparte-skeleton--rect" style="grid-column: 2" aria-hidden="true"></span>
</div>

.aparte-skeleton · .aparte-skeleton--circle · .aparte-skeleton--rect · .aparte-skeleton--text

aparte-divider — horizontal, vertical, optionally labelled

The line is a BORDER, not a background — see the progress bar’s comment above for why: a background disappears under forced-colors, a border does not.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<hr class="aparte-divider">
<div class="aparte-divider aparte-divider--label">
<span class="aparte-divider__label">or</span>
</div>

.aparte-divider · .aparte-divider--label · .aparte-divider--vertical · .aparte-divider__label

aparte-alert — a callout

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<div class="aparte-alert aparte-alert--info" role="status">
<span class="aparte-alert__icon"><aparte-icon name="info"></aparte-icon></span>
<div class="aparte-alert__body">
<p class="aparte-alert__title">Model switched</p>
<p class="aparte-alert__message">The next reply uses the model you just picked.</p>
</div>
<button class="aparte-alert__dismiss" aria-label="Dismiss"><aparte-icon name="close"></aparte-icon></button>
</div>
<div class="aparte-alert aparte-alert--danger" role="alert">
<span class="aparte-alert__icon"><aparte-icon name="alertTriangle"></aparte-icon></span>
<div class="aparte-alert__body">
<p class="aparte-alert__message">The request was refused: no API key is set.</p>
</div>
</div>

.aparte-alert · .aparte-alert--danger · .aparte-alert--info · .aparte-alert--neutral · .aparte-alert--primary · .aparte-alert--secondary · .aparte-alert--success · .aparte-alert--warning · .aparte-alert__body · .aparte-alert__dismiss · .aparte-alert__icon · .aparte-alert__message · .aparte-alert__title

aparte-card — a surface with optional header/body/footer

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<div class="aparte-card">
<div class="aparte-card__header">Usage this month</div>
<div class="aparte-card__body">
<p>412 000 tokens across 38 conversations.</p>
</div>
<div class="aparte-card__footer">Resets on the 1st.</div>
</div>

.aparte-card · .aparte-card__body · .aparte-card__footer · .aparte-card__header

aparte-kbd — a keyboard key

The monospace stack is —aparte-code-font-family, reused rather than declared again: it is already the library’s one answer to “what font is code”, and a key label is exactly that question asked a second time.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
Press <kbd class="aparte-kbd">Ctrl</kbd> + <kbd class="aparte-kbd">K</kbd> to search,
or <kbd class="aparte-kbd">Esc</kbd> to close.

.aparte-kbd

aparté — The mark: a row a list has settled on.

One of the sheets split out of aparte.css. Order matters: src/index.ts imports them in the order listed there, and check:derived-vars reads them concatenated in that same order, the way a browser does.

WHAT IT IS. A chosen option, an accepted answer, the conversation you are in: the one item among its peers that a decision landed on. Before this sheet the select drew it one way (an accent tint plus an inset bar on the start edge), the field choice another (a primary border), the conversation list a third (its own background and weight), and the ask-user receipt none at all — four vocabularies for one meaning, measured in the UI audit of 2026-08-28. The market agrees on two signals: a tint on the ground (Radix, shadcn, Material’s state layer) and a bar on the row’s start edge (GitHub Primer’s ActionList). This recipe is both, on one intent axis, and every row that means “this one” wears it.

THE AXIS. --aparte-mark-intent is the fill, primary by default: chosen. The modifiers set it to the theme’s other masters — success for an answer that was given or a call that was approved, danger for a destructive outcome, neutral for the item you are merely in. --quiet is the outcome that did NOT happen: a declined request, a version superseded — no tint, no bar, the muted voice. The market never strikes through; neither does this.

THE BAR IS DRAWN IN INK. The tint is the fill at 18%, which carries almost no contrast by itself (1.2:1 in the light theme); the bar is the whole non-text signal, so it reads --aparte-mark-intent-ink — the master’s ink, the one the button and the badge already derive for text on a ground. The raw success fill measured 2.27:1 on the light surface, under the 3:1 WCAG asks of a graphic; its ink is 7.1:1.

THE BAR IS A PSEUDO-ELEMENT, NOT A SHADOW. An inset shadow knows only left and right, so a right-to-left row got its bar on the wrong edge unless a document-level [dir="rtl"] rule caught it — which a dir on the row itself, or dir="auto", escapes. inset-inline-start is the start edge whatever the direction, at any level. Nothing shifts: the pseudo-element is out of the flow.

THE MEASURES. --aparte-mark-tint and --aparte-mark-bar live in theme.css so a theme moves every mark at once.

.aparte-mark · .aparte-mark--danger · .aparte-mark--neutral · .aparte-mark--quiet · .aparte-mark--success

aparté — layered surfaces. Ready-made classes for the things that float ABOVE the

transcript: tabs, an accordion, a dropdown menu, a popover, a tooltip.

Drawer and toast are deliberately NOT here: each needs a stack manager — something that owns the order of what floats over the page — and that is the consuming application’s job, not a component stylesheet’s. The dialog was on that list until 0.16.0; it has its own recipe now (dialog.css), because the browser’s <dialog> brings the top layer, the focus trap and Escape with it.

Everything below is the LOOK only. Where and when a surface opens is markup and script the caller owns: a <details open> toggling itself, a positioning library placing a menu next to its trigger. Nothing here reaches for position:fixed on your behalf.

.aparte-popover and .aparte-tooltip are single boxes: drop content straight in and position the element yourself, however the app already places floating things (an absolutely-placed div, a floating-UI anchor, a JS library’s own logic).

This layer is NEUTRAL: it is what a UI library offers, not a summary of what this repo happens to use. A class nothing wears today still ships, because the plugin that needs it is not written yet.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<div class="aparte-tabs" role="tablist" aria-label="Result view">
<button class="aparte-tabs__tab" role="tab" type="button" id="tabs-a-preview" aria-controls="tabs-a-preview-panel" aria-selected="true" tabindex="0">Preview</button>
<button class="aparte-tabs__tab" role="tab" type="button" id="tabs-a-code" aria-controls="tabs-a-code-panel" aria-selected="false" tabindex="-1">Code</button>
</div>
<div class="aparte-tabs__panel" role="tabpanel" id="tabs-a-preview-panel" aria-labelledby="tabs-a-preview">The panel for the selected tab.</div>
<div class="aparte-tabs__panel" role="tabpanel" id="tabs-a-code-panel" aria-labelledby="tabs-a-code" hidden>The panel the other tab opens.</div>
<div class="aparte-accordion">
<details class="aparte-accordion__item" open>
<summary class="aparte-accordion__header">Section title
<aparte-icon name="expand"></aparte-icon>
</summary>
<div class="aparte-accordion__panel">What the section says once it is open.</div>
</details>
</div>
<div class="aparte-menu" role="menu">
<div class="aparte-menu__label">Export as</div>
<button class="aparte-menu__item" role="menuitem">PNG</button>
<div class="aparte-menu__separator"></div>
<button class="aparte-menu__item" role="menuitemcheckbox" aria-checked="true">
Include metadata
</button>
</div>

aparte-tabs — one row of tabs, and the panel under it

Two looks, one behaviour. --underline marks the current tab with a rule beneath it and is the quieter of the two; --segmented puts the whole row in a track and fills the current tab, which reads as a control rather than as navigation. Neither moves anything: aria-selected is what says which tab is current, and the app owns it.

The markup below is the whole contract of the role, and all of it is yours to write: ONE tab stop for the list (tabindex="0" on the selected tab, -1 on the rest), aria-controls from each tab to its role="tabpanel", and aria-labelledby back. The app also owns the ArrowLeft/ArrowRight/Home/End handler that moves the selection and the stop together — @aparte/plugin-artifacts’ card has a working one to copy. A tablist without those is louder than plain buttons and says less: every tab a tab stop, and none of them naming what it opens.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<div class="aparte-tabs aparte-tabs--underline" role="tablist" aria-label="Result view">
<button class="aparte-tabs__tab" role="tab" type="button" id="tabs-u-preview" aria-controls="tabs-u-preview-panel" aria-selected="true" tabindex="0">Preview</button>
<button class="aparte-tabs__tab" role="tab" type="button" id="tabs-u-code" aria-controls="tabs-u-code-panel" aria-selected="false" tabindex="-1">Code</button>
</div>
<div class="aparte-tabs__panel" role="tabpanel" id="tabs-u-preview-panel" aria-labelledby="tabs-u-preview">What the selected tab shows.</div>
<div class="aparte-tabs__panel" role="tabpanel" id="tabs-u-code-panel" aria-labelledby="tabs-u-code" hidden>What the other tab shows.</div>
<div class="aparte-tabs aparte-tabs--segmented" role="tablist" aria-label="Theme">
<button class="aparte-tabs__tab" role="tab" type="button" id="tabs-s-light" aria-controls="tabs-s-light-panel" aria-selected="true" tabindex="0">Light</button>
<button class="aparte-tabs__tab" role="tab" type="button" id="tabs-s-dark" aria-controls="tabs-s-dark-panel" aria-selected="false" tabindex="-1">Dark</button>
<button class="aparte-tabs__tab" role="tab" type="button" id="tabs-s-system" aria-controls="tabs-s-system-panel" aria-selected="false" tabindex="-1">System</button>
</div>
<div class="aparte-tabs__panel" role="tabpanel" id="tabs-s-light-panel" aria-labelledby="tabs-s-light">The light theme's settings.</div>
<div class="aparte-tabs__panel" role="tabpanel" id="tabs-s-dark-panel" aria-labelledby="tabs-s-dark" hidden>The dark theme's settings.</div>
<div class="aparte-tabs__panel" role="tabpanel" id="tabs-s-system-panel" aria-labelledby="tabs-s-system" hidden>Whatever the system asks for.</div>

.aparte-tabs · .aparte-tabs--segmented · .aparte-tabs--underline · .aparte-tabs__panel · .aparte-tabs__tab

aparte-accordion — stacked disclosures

A column of items, each a header you press and a panel it reveals. The header works as a native <summary> inside <details> OR as a <div role="button" aria-expanded> — that choice decides whether the open state is the browser’s or yours, and the CSS does not care either way. It is the whole clickable row, so it takes the same reset a button does.

The chevron turns 180° when the item is open, and there are two selectors for it because there are two ways to be open: aria-expanded on the header for the ARIA pattern, details[open] on the ancestor for the native one. Nothing here opens or closes anything — this is a stylesheet, and the disclosure is the element’s job or yours.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<div class="aparte-accordion">
<details class="aparte-accordion__item" open>
<summary class="aparte-accordion__header">
Shipping
<aparte-icon name="expand"></aparte-icon>
</summary>
<div class="aparte-accordion__panel">Free above 50 €, otherwise 4,90 €. Two to four working days.</div>
</details>
<details class="aparte-accordion__item">
<summary class="aparte-accordion__header">
Returns
<aparte-icon name="expand"></aparte-icon>
</summary>
<div class="aparte-accordion__panel">Thirty days, in the original packaging.</div>
</details>
<details class="aparte-accordion__item">
<summary class="aparte-accordion__header">
Payment
<aparte-icon name="expand"></aparte-icon>
</summary>
<div class="aparte-accordion__panel">Card, PayPal, or an invoice for a company account.</div>
</details>
</div>

.aparte-accordion · .aparte-accordion__header · .aparte-accordion__icon · .aparte-accordion__item · .aparte-accordion__panel · .aparte-icon

aparte-menu — a floating list of commands

The surface a dropdown, a context menu or an overflow menu is drawn on: a bordered, elevated column of rows. Items are the pressable ones, a label groups them, a separator divides them.

The check mark is DRAWN HERE, not by you: an item carrying aria-checked (a menuitemradio or menuitemcheckbox) reserves a gutter, and the glyph shows when the value is "true" — so set the attribute and do not add a ”✓” of your own, or the row shows two. A two-line item wraps its name and its __description in a __body; the gutter then spans both lines.

WHERE it sits is yours. There is no positioning here and no anchoring logic — floating a box next to a trigger needs collision detection and a scroll listener, which is an interaction library’s job, not a stylesheet’s. Same split as the popover and the tooltip below it.

The roles above are not decoration either: this draws a menu, it does not announce one. role="menu" / role="menuitem" and the arrow-key handling that pattern requires are yours to add.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<div class="aparte-menu" role="menu">
<span class="aparte-menu__label">Message</span>
<button class="aparte-menu__item" role="menuitem">Copy</button>
<button class="aparte-menu__item" role="menuitem">Retry</button>
<div class="aparte-menu__separator" role="separator"></div>
<button class="aparte-menu__item" role="menuitem" disabled>Delete</button>
<div class="aparte-menu__separator" role="separator"></div>
<span class="aparte-menu__label">Mode</span>
<button class="aparte-menu__item" role="menuitemradio" aria-checked="true">
<span class="aparte-menu__body">
Ask
<span class="aparte-menu__description">Every tool call waits for you</span>
</span>
</button>
<button class="aparte-menu__item" role="menuitemradio" aria-checked="false">
<span class="aparte-menu__body">
Auto
<span class="aparte-menu__description">Runs everything, reports after</span>
</span>
</button>
</div>

.aparte-menu · .aparte-menu__body · .aparte-menu__description · .aparte-menu__item · .aparte-menu__label · .aparte-menu__separator

aparte-popover — a floating box, and nothing else

Deliberately just a box. A popover’s content is arbitrary — a form, a colour picker, a card of details — so this supplies the surface, border and elevation that say “this is floating above the page” and leaves the layout of what is inside, and where the box itself sits, entirely to the caller.

--aparte-popover-max-width is the one cap it imposes, so arbitrary content cannot grow the box unbounded.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<div class="aparte-popover" role="dialog" aria-label="Filters">
<label class="aparte-field-label" for="q">Search</label>
<input class="aparte-field" id="q" />
<button class="aparte-btn aparte-btn--primary aparte-btn--solid">Apply</button>
</div>

.aparte-popover

aparte-dialog — a modal, drawn on the native <dialog>.

The kit said for a year that a dialog was “deliberately absent — a modal needs a portal and a stack manager”. The browser has had both since 2022: <dialog> with showModal() puts the box in the top layer above everything, traps focus, closes on Escape, hands the focus back, and draws a ::backdrop. So this recipe styles THAT element, and core’s one addition is the wiring — a data-aparte-dialog-open on any control opens the dialog it names, a data-aparte-dialog-close inside one closes it, and a click on the backdrop closes it unless the dialog carries data-aparte-dialog-static. Nothing here reaches for position:fixed on your behalf: the top layer is the browser’s.

Sizes: --sm (24rem), the default (32rem), --lg (48rem); under 30rem of window every size becomes a full-screen sheet, which is what a settings hub is on a phone. The value passed to data-aparte-dialog-close="…" becomes the dialog’s returnValue, read in its native close event.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<dialog class="aparte-dialog" id="settings" aria-labelledby="settings-title">
<div class="aparte-dialog__header">
<h2 class="aparte-dialog__title" id="settings-title">Settings</h2>
<button class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-dialog__close" type="button" aria-label="Close" data-aparte-dialog-close>
<aparte-icon name="close"></aparte-icon>
</button>
</div>
<div class="aparte-dialog__body">
<label class="aparte-field-label" for="endpoint">Endpoint</label>
<input class="aparte-field" id="endpoint" value="http://localhost:11434/v1">
</div>
<div class="aparte-dialog__footer">
<button class="aparte-btn aparte-btn--ghost" type="button" data-aparte-dialog-close>Cancel</button>
<button class="aparte-btn aparte-btn--primary aparte-btn--solid" type="button" data-aparte-dialog-close="saved">Save</button>
</div>
</dialog>
<button class="aparte-btn aparte-btn--surface" type="button" data-aparte-dialog-open="settings">Open settings</button>

.aparte-dialog · .aparte-dialog--lg · .aparte-dialog--sm · .aparte-dialog__body · .aparte-dialog__close · .aparte-dialog__footer · .aparte-dialog__header · .aparte-dialog__title

aparte-tooltip — a short label, with an arrow

A tooltip has to be legible against whatever it happens to sit on top of — a bright image, a dark code block, either app theme — so its colours are fixed literals rather than the themed surface/text tokens. This box does NOT follow the palette, on purpose, and skips the dark-theme override block for that reason; the artifact “paper” tokens are the same decision made for the same reason.

data-side places it — top, bottom, start or end — against the .aparte-tooltip-anchor that wraps the trigger, --aparte-tooltip-gap away, and turns the arrow to match. The two inline styles that used to do this in the example were a knob missing, not a division of labour: a demo that needs inline styles to work is a recipe with a parameter it forgot. What stays yours is the viewport: nothing here flips a tooltip that would leave the screen — that needs script, a positioning library’s job. Without data-side the box is not positioned at all, as before, and its placement is yours (the one inline style left in the example is the demo’s own margin).

pointer-events: none is not a detail: a tooltip describes what the pointer is over, so it must never itself become that thing, or hovering it re-triggers the hover that produced it.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<span class="aparte-tooltip-anchor" style="margin-block-start: 2.5rem; margin-inline-start: 5rem">
<div class="aparte-tooltip" data-side="top" role="tooltip">
Copy to clipboard
<span class="aparte-tooltip__arrow"></span>
</div>
<button class="aparte-btn aparte-btn--icon" aria-label="Copy"><aparte-icon name="copy"></aparte-icon></button>
</span>

.aparte-tabs--segmented · .aparte-tabs__tab · .aparte-tooltip · .aparte-tooltip-anchor · .aparte-tooltip__arrow

aparté — the shell: the chrome a whole application puts around the chat. A sidebar

of conversations, a header, and the grid that holds them beside the transcript — what a ChatGPT-style page is made of, so a site can be built on aparté alone.

One of the sheets src/index.ts imports in cascade order; check:derived-vars reads them concatenated in that same order, the way a browser does.

aparte-sidebar — the column beside the chat.

A sized column that becomes a drawer under 48rem of window. The recipe draws the column and its four regions; <aparte-sidebar> adds the behaviour (collapse, the drawer, the search filter) and sets data-drawer on itself from the media query, which is the one attribute the rules below read.

The element wears the same class and adds the behaviour; <aside class="aparte-sidebar"> is the look alone, which is what this page shows.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<aside class="aparte-sidebar">
<div class="aparte-sidebar__header">
<span class="aparte-sidebar__brand">aparté</span>
<button class="aparte-btn aparte-btn--icon aparte-btn--sm" type="button" aria-label="New chat">
<aparte-icon name="edit"></aparte-icon>
</button>
</div>
<div class="aparte-sidebar__search aparte-field-group">
<input class="aparte-field aparte-field--sm" type="search" placeholder="Search" data-aparte-sidebar-search>
</div>
<div class="aparte-sidebar__body">
<aparte-conversation-list></aparte-conversation-list>
</div>
<div class="aparte-sidebar__footer">
<span class="aparte-avatar aparte-avatar--sm">P</span> Paul
</div>
</aside>

.aparte-sidebar · .aparte-sidebar__body · .aparte-sidebar__brand · .aparte-sidebar__footer · .aparte-sidebar__header · .aparte-sidebar__scrim · .aparte-sidebar__search

aparte-app-header — the bar above the chat.

A recipe and nothing more, because a header has no behaviour of its own: a toggle for the sidebar (which <aparte-sidebar> wires from the data-aparte-sidebar-toggle attribute), the conversation’s title, and an actions zone pushed to the end — a model selector, a share button, whatever the page needs. The toggle shows only under 48rem, where the sidebar is a drawer; above it the column is simply there.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<header class="aparte-app-header">
<button class="aparte-btn aparte-btn--icon aparte-app-header__toggle" type="button" aria-label="Toggle the sidebar" data-aparte-sidebar-toggle><aparte-icon name="menu"></aparte-icon></button>
<span class="aparte-app-header__title">Deploy checklist</span>
<div class="aparte-app-header__actions">
<span class="aparte-tag">gpt-4.1</span>
<button class="aparte-btn aparte-btn--sm aparte-btn--ghost" type="button">Share</button>
</div>
</header>

.aparte-app-header · .aparte-app-header__actions · .aparte-app-header__title · .aparte-app-header__toggle

aparte-app-shell — sidebar beside, header above, the chat in the rest.

A recipe: two columns and two rows, the sidebar spanning both rows, the header and the main area stacked in the second column. The chat fills __main, which is what gives it the height it needs to scroll (see the layout guide). Size the shell — height: 100dvh for a page — and everything inside follows. Under 48rem the sidebar leaves the grid to become a drawer, and the header’s toggle appears.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<div class="aparte-app-shell" style="height: 24rem">
<aside class="aparte-sidebar">
<div class="aparte-sidebar__header">
<span class="aparte-sidebar__brand">aparté</span>
<button class="aparte-btn aparte-btn--icon aparte-btn--sm" type="button" aria-label="New chat"><aparte-icon name="edit"></aparte-icon></button>
</div>
<div class="aparte-sidebar__search aparte-field-group">
<input class="aparte-field aparte-field--sm" type="search" placeholder="Search">
</div>
<div class="aparte-sidebar__body">
<div class="aparte-conv-group" role="group" aria-label="Today">
<div class="aparte-menu__label aparte-conv-group__label" aria-hidden="true">Today</div>
<div class="aparte-menu__item aparte-conv-item aparte-conv-item--active"><button class="aparte-conv-item__select" type="button"><span class="aparte-conv-item__title">Deploy checklist</span></button></div>
<div class="aparte-menu__item aparte-conv-item"><button class="aparte-conv-item__select" type="button"><span class="aparte-conv-item__title">Rename the staging bucket</span></button></div>
</div>
<div class="aparte-conv-group" role="group" aria-label="Yesterday">
<div class="aparte-menu__label aparte-conv-group__label" aria-hidden="true">Yesterday</div>
<div class="aparte-menu__item aparte-conv-item"><button class="aparte-conv-item__select" type="button"><span class="aparte-conv-item__title">Why is total a float?</span></button></div>
</div>
</div>
<div class="aparte-sidebar__footer"><span class="aparte-avatar aparte-avatar--sm">P</span> Paul</div>
</aside>
<header class="aparte-app-header">
<span class="aparte-app-header__title">Deploy checklist</span>
<div class="aparte-app-header__actions"><span class="aparte-tag">gpt-4.1</span></div>
</header>
<main class="aparte-app-shell__main">
<aparte-chat>
<aparte-chat-viewport>
<aparte-chat-bubble message-id="u1" data-role="user" content="Walk me through the deploy."></aparte-chat-bubble>
<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant" content="Build, run the gate, tag, then push the tag — the workflow publishes from it."></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>
</main>
</div>

.aparte-app-header · .aparte-app-shell · .aparte-app-shell__main · .aparte-sidebar · .aparte-split

aparte-split — two panes and a draggable seam. The chat in one, your pane in the

other: a preview frame, an editor, an artifact card.

The grid is the whole mechanism. --aparte-split-position is the primary pane’s size; --aparte-split-min and --aparte-split-max are clamp bounds, so the browser does the clamping and nothing in JS parses a unit — px, %, rem and ch all work. Used without &lt;aparte-split> this is a static split: set the position from your own media query and there is no drag, which is the Canvas shape.

A pane CONTAINS a chat; a chat never contains a split.

The element wears the same class and adds the behaviour (drag, arrow keys, the APG separator’s ARIA, one pane at a time under a breakpoint); <div class="aparte-split"> is the look alone, which is what this page shows.

Width Open in a tab
The classes below, on plain markup, in their own document — so what you are looking at is core's stylesheet and nothing else.
<div class="aparte-split" style="height: 18rem">
<div>the chat</div>
<div class="aparte-split__handle"></div>
<div class="aparte-split__pane">the pane</div>
</div>

.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

These are on the elements aparté renders. They are listed so that nothing it emits is a surprise in your inspector, and so a theme can target one — but what each region is is explained on its element’s page, not here.

SheetClasses
styles/base.css.aparte-actions-left .aparte-actions-right .aparte-bottom-spacer .aparte-dot .aparte-dots .aparte-footer .aparte-header .aparte-link .aparte-link--muted .aparte-sr-only
primitives/select.css.aparte-optgroup-chevron .aparte-optgroup-header .aparte-optgroup-loader .aparte-select-chevron .aparte-select-dropdown .aparte-select-label .aparte-select-label-sizer .aparte-select-label-text .aparte-select-options .aparte-select-search .aparte-select-trigger .aparte-spinner-small .aparte-status-dot
primitives/progress-spinner.css.aparte-spinner-fill .aparte-spinner-track
components/shell.css.aparte-chat-container .aparte-chat-container--auto-center .aparte-composer-shell .aparte-messages-wrapper .aparte-scroll-btn .aparte-scroll-btn--hidden .aparte-status-container .aparte-status-content .aparte-status-text .aparte-viewport--framework .aparte-viewport-container
components/bubble.css.aparte-action-bar .aparte-action-btn .aparte-action-button .aparte-action-edit-cancel .aparte-action-edit-save .aparte-avatar .aparte-body .aparte-branch-label .aparte-branch-next .aparte-branch-picker .aparte-branch-prev .aparte-footer .aparte-header .aparte-message .aparte-message-content .aparte-message-streaming .aparte-messages-wrapper .aparte-name .aparte-timestamp
components/composer.css.aparte-cc-button .aparte-chat-container--auto-center .aparte-ci-editor .aparte-composer-footer .aparte-composer-row .aparte-composer-shell .aparte-editor .aparte-has-content .aparte-input-container .aparte-input-footer .aparte-input-upper .aparte-input-wrapper .aparte-is-dragover .aparte-message .aparte-model-select .aparte-model-selector .aparte-provider-select .aparte-send-button .aparte-thumb .aparte-thumb__remove .aparte-viewport--framework .aparte-viewport-container
segment/thinking.css.aparte-dot .aparte-dots .aparte-segment-thinking .aparte-segments .aparte-thinking-header .aparte-waiting
segment/code.css.aparte-code-copy .aparte-code-filename .aparte-code-header .aparte-code-header-filler .aparte-code-language
segment/tool-call.css.aparte-segment-tool-call .aparte-tool-detail .aparte-tool-icon .aparte-tool-label .aparte-tool-name .aparte-tool-part-body .aparte-tool-part-label .aparte-tool-spinner .aparte-tool-state .aparte-tool-summary .aparte-tool-toggle
segment/error.css.aparte-segment-error__details
segment/text.css.aparte-segment-code .aparte-segment-error .aparte-segment-text
components/elicitation.css.aparte-approval-args .aparte-approval-args-label .aparte-approval-instruction .aparte-approval-option .aparte-approval-options .aparte-elic-body .aparte-elic-control .aparte-elic-desc .aparte-elic-dismiss .aparte-elic-field .aparte-elic-message .aparte-elic-option .aparte-elic-option--command .aparte-elic-option--recommended .aparte-elic-option-badge .aparte-elic-option-body .aparte-elic-option-desc .aparte-elic-option-title .aparte-elic-options .aparte-elic-other-input .aparte-elic-panel .aparte-elic-panel--stepped .aparte-elic-skip .aparte-elic-step .aparte-elic-step__mark .aparte-elic-steps .aparte-elic-text .aparte-elic-title .aparte-field-choice--selected
components/conversation.css.aparte-conv-group .aparte-conv-item .aparte-conv-item--active .aparte-conv-item--archived .aparte-conv-item__input .aparte-conv-item__more .aparte-conv-item__select .aparte-conv-item__title .aparte-conv-menu .aparte-conv-menu__actions .aparte-conv-menu__confirm .aparte-conv-menu__item--danger .aparte-conv-menu__question
components/suggestions.css.aparte-suggestion .aparte-suggestions
components/context.css.aparte-context .aparte-context--ring .aparte-context__ring .aparte-context__text .aparte-context__track .aparte-context__value .aparte-context__value--empty .aparte-progress .aparte-progress__bar
components/scroll-rail.css.aparte-chat-container .aparte-scroll-rail__list .aparte-scroll-rail__tick
styles/prose.css.aparte-code-content-wrapper .aparte-content .aparte-segment-content .aparte-thinking-content
styles/responsive.css.aparte-action-bar .aparte-action-button .aparte-avatar .aparte-composer-row .aparte-conv-item__more .aparte-message .aparte-messages-wrapper .aparte-scroll-btn .aparte-send-button .aparte-split__handle .aparte-thumb__remove