Single-purpose UI elements.
These are common, non-complex UI elements that just need a class or two. Forms, cards, buttons, and more.
Button styling for `<button>` elements and `.button` class for links.
When to use: Buttons and links styled as actions.
Classes: .button, .primary, .success, .warning, .error, .ghost, .minimal, .dark, .light, .contrast, .mini
.dark, .light, and .contrast keep a fixed surface regardless of theme. Use them on buttons that need to sit on a particular surface (e.g., a .dark hero) and stay legible.
.reset is a general-purpose utility (not a button variant) for stripping native chrome from any element. It removes background, border, radius, shadow, and padding while inheriting typography and color — useful when you want a <button> to behave like a bare interactive surface.
Compact square button for an icon-only action, with auto glyph sizing and an aria-label requirement.
When to use: Toolbar controls, close affordances, overflow menus, and any action carried by an icon alone.
Classes: .icon-button, button
.icon-button is a square button for an icon-only action. It builds on the base
<button> styling and standardizes the things that were previously re-derived by
hand: glyph size, square padding, and the focus ring. Compose it with any button
variant (.primary, .ghost, .minimal, .mini).
Icon-only controls must be named for assistive tech, so always supply an
aria-label (the same convention .tip relies on for its label text).
.icon-button composes with the button variants. Use .mini for a tighter
footprint and .ghost / .minimal for low-emphasis toolbar controls.
A labelled <button> whose only child is an <svg> collapses to a square
footprint with no class at all. This keeps one-off icon triggers terse while
still requiring an accessible name.
<button> or .button is sized from --button-icon-size
(default 1.15em), so a label+icon button and an icon-only button share one
optical glyph size.--icon-button-pad; .mini tightens it.aspect-ratio: 1, so it never drifts wider
than it is tall regardless of the icon.aria-label — the auto-square rule only
triggers when the label is present, nudging you toward an accessible name.<svg> should be aria-hidden="true" so the label is the single
accessible name.Circular, fixed-position primary action pinned above the safe area.
When to use: A single, high-emphasis action that should stay reachable while content scrolls (compose, add, new message).
Classes: .fab, .button, .circle
The .fab is a floating action button: a circular, elevated control pinned to
the bottom-inline-end corner of the viewport, clear of the device safe area. It
is composition-first — .button supplies the surface, color variant, hover lift,
and focus ring, .circle supplies the round footprint, and .fab adds only the
fixed positioning and a stronger elevation shadow.
aria-label — the same
icon-only naming convention .tip and .icon-button rely on.<svg> should be aria-hidden="true" so the label is the one
accessible name.position: fixed, pinned with inset-block-end: calc(var(--safe-bottom) + var(--pad-l))
and inset-inline-end: calc(var(--safe-right) + var(--pad-l)) so it clears the
home indicator and rounded display corners.var(--z-sticky) to sit above scrolling content, matching the
.bottom-nav stacking convention.var(--shadow-4) — one step stronger than .bottom-nav since the
FAB floats higher in the stack.--size: 56px (the Material-standard FAB size); override
--size for a mini or extended variant.When a .bottom-nav is present, lift the FAB so it clears the tab bar by
overriding its bottom inset:
.button and degrades automatically under the global
prefers-reduced-motion guard — .fab introduces no animation of its own..button.<button>, it works with zero JavaScript and is fully
keyboard and screen-reader operable.Mutually exclusive compact choices backed by native radio inputs.
When to use: Switching one local view or mode when every option can be named with a short label.
Classes: .segmented-control, .compact, .full
Use a fieldset with a real legend. Each direct label contains one radio. The radios retain their shared name, form value, arrow-key behavior, and validation semantics; Graffiti only changes their presentation.
.compact tightens the labels for dense toolbars..full makes equal segments fill the available inline size.Visual contracts for enhanced text selection, listbox popups, and option states.
When to use: Filtering and selecting from a choice set when a native select or datalist cannot meet the interaction requirements.
Classes: .combobox, .listbox, .option
Decks owns filtering, aria-expanded, aria-controls, aria-activedescendant, active-option movement, selection, Escape, and focus return. Graffiti expects this structure:
Use data-active="true" for the active-descendant row, aria-selected for committed selection, and aria-disabled for unavailable options. Add popover to .listbox when it should use the top layer; otherwise Decks may toggle hidden, .open, or data-state. Prefer native <select> for finite non-searchable choices and <datalist> when its browser-provided behavior is sufficient.
Multi-value input frame composed from tags, a combobox input, and a listbox.
When to use: Adding and removing several short values such as labels, recipients, or filters.
Classes: .tag-input, .tag, .listbox, .option
.tag-input contains existing .tag values and one borderless text or combobox input. Each removal control needs a value-specific accessible label. Decks owns tokenization, duplicate prevention, Backspace behavior, selection, and announcements.
The frame exposes focus-within, invalid, and disabled visuals. Keep the source-order input reachable after existing tags. Do not make an entire tag removable without a dedicated focusable control.
Interactive pill-shaped elements for filters, categories, and selections.
When to use: Selectable pills for filters and segmented choices.
Classes: .chip, .selected, .mini
Use for multi-select interfaces, tag filters, or skill selectors.
Icons are automatically sized to 1em.
border-radius: var(--br-xxl)var(--border-1)--fl: -1 (smaller text)Native date input first, with an enhanced anchored calendar surface when product requirements exceed the platform picker.
When to use: Date entry, date selection, or range workflows that need either native browser UI or Decks-owned calendar behavior.
Classes: .date-picker, .calendar, .calendar-header, .calendar-grid, .calendar-day
Start with a labeled native input:
Only add an enhanced .calendar[popover] for range selection, unavailable dates, or product-specific month navigation. .date-picker anchors the trigger. .calendar-header contains previous/title/next controls. .calendar-grid remains a semantic table, and each .calendar-day is a button.
Use aria-current="date" for today, aria-pressed or data-selected for selection, data-range-start / data-range-end for range endpoints, .outside for adjacent-month dates, and native disabled for unavailable dates. Decks owns localized labels, roving focus, Page Up/Down, Home/End, month arithmetic, and focus return.
Subtle category labels with customizable colors.
When to use: Status or category labels.
Classes: .tag, .success, .warning, .error, .info, .muted
Use for metadata, categories, or status indicators.
For custom categories, use --tag-color as a fallback when semantic variants are not a fit:
Available colors: --red, --orange, --yellow, --green, --teal, --blue, --indigo, --purple, --pink, --gray, --slate
Icons are automatically sized to 1em.
--tag-color via OKLCH lightness/chroma adjustments (theme-aware in light and dark)--tag-color--fl: -1 (smaller text)border-radius: var(--br-xxl)<a>/<button>) lift 1px on hover (translate: 0 -1px)Five-star rating in two zero-JS forms — a read-only display meter and a native-radio input.
When to use: Showing an average score or collecting a star rating without JavaScript.
Classes: .rating
.rating ships two forms that share the same star visuals. Pick by whether the
value is read-only or editable. Both work with native HTML + CSS only and need
no JavaScript.
Put a --rating value (0–5, decimals allowed) on a <span class="rating">. The
filled portion is clipped to --rating / 5, so half- and quarter-stars render
faithfully. Because the stars are decorative paint, give the element an
accessible name with role="img" + aria-label so the score is announced.
--warning (the semantic amber token); the empty track uses
--fg-2. Override --rating-on / --rating-off to retint.--rating-size (defaults to 1.25em).Wrap five radio + label pairs in a <fieldset class="rating">. List them in
reverse order (5 down to 1) so the general-sibling combinator can fill the
checked star and every star before it. It collects a real form value, supports
keyboard selection, shows a hover preview, and degrades to plain native radio
buttons if the styling is unavailable.
<label> carries an aria-label ("3 stars") so the choice is announced.display: none); the active
label gets the standard focus ring on :focus-visible.prefers-reduced-motion via the global motion guard.role="img" + aria-label is the single source of truth for the
value — keep the label in sync with --rating.fieldset + legend group the radios; submit the chosen
value like any other radio group.Emoji reaction picker on a native select, degrades to a normal dropdown.
When to use: Letting people react to a post, comment, or message with a single emoji.
Classes: .reactions
.reactions turns a native <select> into a horizontal emoji reaction bar
using the customizable select API (appearance: base-select). The whole
control is one class on the <select> — the trigger shows the current
reaction, and opening it reveals the bar of emoji options. No JavaScript.
Three pieces of native markup make the customizable select work — none of them are extra Graffiti classes:
<button><selectedcontent></selectedcontent></button> — the trigger. The
<selectedcontent> mirrors the chosen <option>, so the trigger always
shows the current reaction.<option> — one per reaction. The emoji is the visible glyph; a trailing
.visually-hidden text label ("Like", "Love", …) gives each option a real
accessible name so it isn't announced as "thumbs-up sign".class="reactions" on the <select> — the only Graffiti class involved.Because it's a real <select>, you get keyboard navigation, focus management,
and a submittable form value for free. The chosen reaction posts under the
control's name.
Where customizable select isn't supported, the <button> /
<selectedcontent> children are ignored and the element renders as an ordinary
<select> dropdown of the same emoji options — a fully working, accessible
fallback with no JavaScript shim.
<select> an aria-label (or an associated <label>) — the
reactions themselves are emoji, so the control needs its own name..visually-hidden text label inside every <option> so each reaction
is announced by name in both the styled bar and the native fallback.:focus-visible; the
open/close entrance is short-circuited under prefers-reduced-motion by the
global motion guard.--reaction-size (default 1.35rem) sets the trigger glyph size; the picker
scales its option emoji off the same value.position-area: block-start +
justify-self: anchor-center) and flips below via
position-try-fallbacks: flip-block when there isn't room.Circular avatar for user images or initials.
When to use: User photos or initials with size variants.
Classes: .avatar, .xs, .s, .l, .xl, .bordered
border-radius: 50%)object-fit: cover--fg-1)Container styles for cards and content blocks.
When to use: Container surface styles and quick panel variants.
Classes: .box, .glow, .semi-gloss, .ghost, .invisible
.box - Tint background, padding, border-radius, border.box.glow - Applies var(--box) — a soft outer drop shadow plus two inset highlights for a subtle dimensional sheen.box.semi-gloss - Gradient background, premium feel.box.ghost - Transparent, border outline only.box.invisible - No visual styling, just structureInformational boxes for tips, warnings, errors, and success messages.
When to use: Inline informational or status callout blocks.
Classes: .callout, .warning, .error, .success, .ghost, .fill, .callout.stack
The first direct <svg> child is pulled into the gutter and colored with
--callout-accent:
Add .stack directly to the callout to align children to the start and stack them with consistent spacing:
A nested <div class="stack"> works too if you need finer control over which children stack.
--callout-tint - Background color (applies to .fill only)--callout-accent - Icon colorA padded surface for grouped content. Reach for header, footer, or media only when you need a divided bar or edge-to-edge media.
When to use: Grouped content, linked previews, and pricing tiles.
Classes: .card, .card.linked, .card.featured
The card itself is the padded surface — drop content directly inside. No wrapper, no body class.
Children stack vertically with a small built-in gap. Override it inline with --gap when you want more breathing room:
Add a direct <header> or <footer> only when you want a divided bar with a separator. They bleed edge-to-edge through the card's padding automatically.
Direct <img>, <picture>, or <figure> children bleed edge-to-edge too. At the top or bottom of the card they extend through that side's padding.
.card is the padded surface. Don't wrap content in an extra <div>.gap: var(--gap, var(--vs-s)). Override with style="--gap: var(--vs-m);".<header>, <footer>, and <img>/<picture>/<figure> direct children bleed to the card edge — use them only when you actually want that treatment.Chat-friendly message container with configurable colors, width, and spacing.
When to use: Chat message presentation and conversation snippets.
Classes: .bubble, .bubble.thinking, .bubble.streaming, .chat-thread, .chat-thread.flowing, .chat-row, .chat-row.self, .chat-message, .chat-composer
.bubble is a rounded chat container with configurable padding, max-width, and child flow spacing.
Use these helpers to build complete chat threads:
.chat-thread - Vertical message stack with configurable spacing/padding.chat-row - Left-aligned row.chat-row.self - Right-aligned row.chat-message - Width-constrained wrapper for each message.chat-composer - Composer row where .input-group expands to fill spaceTwo state modifiers for assistant turns:
.bubble.thinking — dashed border, italic, muted color. For reasoning / system thought..bubble.streaming — appends a blinking caret cursor. Pure CSS (@keyframes).For long-form / editorial agents the conversation can read as one document rather than a stack of bubbles. Each turn becomes a row in a single readable column:
--bubble-bg - Bubble background color--bubble-border - Bubble border color--bubble-max-inline - Max bubble width--bubble-pad-block - Block-axis padding--bubble-pad-inline - Inline-axis padding--bubble-radius - Corner radius--bubble-flow-space - Spacing between child elementsNarrow vertical column of icon buttons with active state and optional status dot.
When to use: Workspace shells, agent switchers, tool palettes — any vertical nav sliver next to a wider sidebar.
Classes: .icon-rail, .icon-rail > .brand, .icon-rail > .status, .icon-rail > .spacer
.icon-rail is a narrow vertical column of icon buttons. Drop in agent
switchers, tool palettes, workspace shells. Pair with .layout-rail
for a rail + sidebar + main app shell.
.icon-rail — vertical container (inline-size: --rail-size, default 4rem)..brand — square top-of-rail mark on the primary color. Place a logo or sigil.> a, > button — icon rows. Active row uses aria-current="page" or
aria-pressed="true"; hover and focus pick up --fg-1 / --shadow-1.> .status — optional 8px green dot in the bottom-trailing corner of a row,
for "currently active" / "online" affordance.> .spacer — flex: 1 filler to push the next children to the bottom.Sits left of a .layout-sidebar, sub-sidebar (.chat-list or similar), or
inside .layout-rail:
--rail-size — width of the rail (default 4rem)Horizontal scrolling with CSS scroll-snap. No JavaScript required.
When to use: Horizontal scroll-snap for cards or media strips.
Classes: .carousel
display: flex with horizontal overflowscroll-snap-type: x mandatory for snappingscroll-snap-align: startscrollbar-width: thin)Compact card with mono label, status slot, and optional pre body — for tool calls, deploy logs, build steps.
When to use: AI tool calls, deploy / CI logs, build steps, activity feeds — any single-row transcript line that may expand to show payload.
Classes: .log-card, .log-card > header, .log-card > header > .label, .log-card > header > .status, .log-card > pre
.log-card is a compact card with a mono label header, a status slot,
and an optional <pre> body. Reads as a single row that may expand.
General-purpose transcript line — not chat-specific.
> header — mono label row with an icon, a .label (monospace), and an
optional .status pushed to the trailing end. Status is meant for short
affordances like ✓ 412ms, … running, × failed.> pre — optional payload. Wraps at the card edge; readable but contained.<pre> is just
the most common.Sits inside a .chat-message for AI tool calls, or anywhere a
single-row-with-payload affordance is needed:
Vertical scrolling with CSS scroll-snap. Like carousel but vertical.
When to use: Vertical scroll-snap list or feed.
Classes: .reel
--reel-height - Container height (default: 80vh)--gap - Gap between panels (default: 1rem)display: flex with flex-direction: columnscroll-snap-type: y mandatory for snappingscroll-snap-align: startResponsive table wrapper with clean data table styling.
When to use: Responsive table wrapper and default table styling.
Classes: .table, .table.zebra, .table.sticky
Add .sticky to the wrapper to pin the header row while the body scrolls vertically. The wrapper becomes a vertical scroll context capped by --table-max-height (defaults to 70vh), and each <th> uses position: sticky so it stays visible on top of the scrolling cells:
position: sticky does all the work.<th> cells get an opaque var(--bg) so scrolling rows do not show through..sticky composes with .zebra on the same wrapper.--table-max-height (e.g. --table-max-height: 24rem).A future enhancement could add a scroll-shadow under the header that appears only while the body is scrolled, using scroll-state container queries (@container scroll-state(scrollable: top)); it is deferred until that feature has broad browser support.
The .table wrapper provides:
overflow-x: auto for responsiveness--table-border - Custom border-radius for wrapperComplete visual states for sortable, selectable, actionable, empty, loading, compact, sticky data tables.
When to use: Interactive tabular datasets whose behavior is managed by Decks or another table engine.
Classes: .data-table, .data-table-toolbar, .data-table-sort, .data-table-actions, .data-table-empty, .data-table-loading
.data-table composes a .data-table-toolbar with the existing .table > table contract. Decks owns TanStack state, callbacks, keyboard behavior, and server coordination.
aria-sort on the <th> and a .data-table-sort button inside it.aria-selected="true" on selected rows. Selection checkboxes retain value-specific labels..data-table-actions on an action cell or toolbar group..data-table-empty cell for filtered zero-results..data-table-loading with .skeleton cells, and set aria-busy="true" on .data-table..compact tightens cell padding. .sticky bounds the table wrapper and pins headers.The toolbar responds to its own container, not the viewport. Tables continue to scroll horizontally on narrow hosts. Never replace native table semantics with a grid of generic elements for styling convenience.
Native progress and meter elements plus canonical empty, skeleton, spinner, and busy-button states.
When to use: Communicating work in flight, known-range values, loading geometry, or the absence of records.
Classes: .progress, .meter, .signaling, .empty, .skeleton, .spinner
Choose by meaning:
<progress class="progress"> is work in flight. Include value for determinate progress and omit it for indeterminate progress.<meter class="meter"> is a current value in a known range. .signaling opts into success/warning/error paint based on the native range attributes..spinner is a compact indeterminate indicator. Give it role="status" and an accessible label unless adjacent text already announces loading..skeleton preserves content geometry. Mark skeleton shapes aria-hidden="true" and put aria-busy="true" plus an accessible loading label on the containing region. .text and .circle are shape modifiers..empty is a centered no-content composition with optional icon, heading, body, and .form-actions. .compact fits bounded cards and table regions.button[aria-busy="true"] gets the canonical busy indicator and stops accepting pointer input. Application behavior must still prevent duplicate submissions. Spinner and skeleton animation stops under reduced motion while their semantic loading text remains.
Native HTML `<dialog>` element with open/close animations.
When to use: Native modal flows and confirmations.
Classes: dialog, .close
No JavaScript required when using HTML invokers.
commandfor points to the dialog's idcommand="show-modal" opens the dialog as a modalcommand="close" closes the dialogThe .close class creates a circular red button. When it is a direct child of a <dialog>, an extra rule pins it to the top-right corner (slightly overlapping the dialog's top edge). Used outside a <dialog>, it's just the circular button — position it yourself.
max-width: 40ch - Character-based width for good proportionsPure CSS tabs using `<details>` and `<summary>` with CSS Grid and Subgrid.
When to use: CSS-only tabbed content using details/summary.
Classes: .tabs, .tabs.boxed, .tabs.pill
No JavaScript required.
name attribute on <details> ensures only one tab can be open at a time (native HTML behavior)--n CSS variable positions each tab's summary in the correct grid column--tab-count on the container sets the number of columns (default: 3)name attributeopen to the tab that should be visible initially--tab-count to match@starting-styleNon-interactive help shown on pointer hover and trigger focus. No JavaScript required.
When to use: Brief supplemental descriptions that do not contain controls or essential information.
Classes: .tip, .tooltip, .tooltip-trigger, .tooltip-content, .bottom, .left, .right
Graffiti has one accessible tooltip contract and one decorative shorthand. Use the explicit rich pattern for supplemental help. Use the pseudo pattern only to visually repeat an icon control's accessible name.
The trigger must be focusable, keep its normal accessible name, and reference the tooltip by id through aria-describedby. The tip must use role="tooltip".
The trigger's accessible name remains "Save" and the tooltip supplies its description. The hidden tip uses visibility: hidden, so its role="tooltip" node does not sit in the normal accessibility-tree reading order. aria-describedby still lets assistive technology expose the text as the trigger's description; that relationship is intentional.
Rich means formatted, non-interactive text such as strong, code, or multiple lines. A tooltip must not contain links, buttons, inputs, or other controls. Use a popover or disclosure when users need to interact with the revealed content.
Position modifiers go on the wrapper:
For an icon-only control, .tip[data-tooltip] can visually repeat the control's explicit accessible name without using that naming attribute as CSS content:
data-tooltip is presentational and must exactly match the control's accessible name. It is not an accessible name or description, and the generated pseudo-element does not use role="tooltip". Do not put aria-label on a control that already has visible text; that can replace its visible name for screen-reader users. Use the rich pattern when a visible-label control needs supplemental help.
Position modifiers (bottom, left, right) go on the icon control for this pseudo pattern.
pointer-events: none..open, data-state="open", or data-state="closed"; these produce the same visual states as pointer/focus activation.--tooltip-offset, --tooltip-show-delay, and --tooltip-hide-delay are registered override tokens for per-instance geometry and timing.:hover or :focus-within. Pressing Escape therefore does not dismiss a tooltip while the pointer or focus remains on its trigger. If Escape dismissal is required because the tip can obscure other content, add an application-level keyboard enhancement or use a native popover/disclosure pattern instead.anchor-scope, position-anchor, position-area) with .tooltip-trigger as the canonical anchorvar(--bg)var(--border-1)var(--shadow-3)30ch (wraps longer text):hover and :focus-visible / :focus-withinvisibility: hidden while closed so rich tooltip nodes do not create stray reading-order contentvar(--d-fast) / var(--ease-smooth))aria-label-Driven Pseudo Tips.tip[aria-label] no longer generates visible content. For icon-only controls, keep the naming aria-label and add an identical data-tooltip. For controls with visible text, remove any replacement aria-label and use .tooltip-trigger plus .tooltip-content when supplemental help is needed. The previous rich .tip child remains a compatibility alias during the migration, but new Decks output uses .tooltip-content. Existing rich tips must add role="tooltip", a unique id, and the trigger's matching aria-describedby; move interactive content to .popover or a disclosure.
Navigation list with clickable rows for settings pages, menus, and navigation indexes.
When to use: Grouped list-style navigation rows.
Classes: .list-nav
Each item is a pill-shaped card with subtle shadow.
Items are direct <a> or <button> children of .list-nav. No additional classes needed.
Each item can contain:
<svg> - Icon on the left (sized to 1.25em)<small> - Optional description (muted, smaller text)Each item automatically gets:
var(--br-xxl) rounded cornersvar(--shadow-2) for elevated card appearancevar(--bg) for proper themingvar(--pad-m) spacing between itemsvar(--border-1) border for definition| Feature | List Nav | Sidebar Nav |
|---|---|---|
| Item style | Individual cards with shadow | Flat items in a list |
| Description | Supported via <small> | Not supported |
| Use case | Standalone navigation rows | Sticky sidebar menus |
| Nesting | Not supported | Supports details/summary |
| Visual weight | Higher (cards with shadow) | Lower (compact) |
<nav> container<a> for navigation links, <button> for actionsdisabled attributearia-disabled="true" when neededAccessible toggle/switch input using native checkbox.
When to use: Boolean settings with checkbox semantics.
Classes: .toggle, .compact
<span id="forms">
--toggle-color - Color when checked (default: var(--primary))--toggle-width - Track width--toggle-height - Track heightThe toggle uses a native checkbox, so it:
prefers-reduced-motionInput field with connected button.
When to use: Input plus attached action button patterns.
Classes: .input-group, .affix, .input-group.stack-mobile
Use for copy-to-clipboard, search with button, URL sharing, or any input that needs an action.
Add an inert .affix child for a leading currency symbol or a trailing unit. The affix shares the input's fill, hairline border, and radius so the seam reads as one continuous control. It is non-interactive (pointer-events: none, user-select: none) — purely a visual adornment, with the real value living in the input.
.affix as the first child for symbols like $ or €..affix as the last child for units like kg, %, or USD.type="number" plus inputmode so mobile keyboards match the expected entry.Add .stack-mobile to break the group into a vertical stack below 640px. Each child reclaims a full border-radius so the input and button read as separate controls.
.primary, .ghost, etc.)Search input with icon positioned inside.
When to use: Search field with icon and compact action behavior.
Classes: .search
Get icons from Phosphor Icons. The magnifying glass icon shown above is the "MagnifyingGlass" icon.
var(--fg-3)) so the user's typed text reads as the primary contentDrag-and-drop file upload zone with click-to-upload fallback.
When to use: Drag-and-drop upload zones with native fallback.
Classes: .dropzone, .dragover
Get icons from Phosphor Icons. The upload icon shown above is the "UploadSimple" icon.
var(--pad-xxxl), radius: var(--br-l) (hardcoded; theme by editing the source or wrapping)2px dashed var(--fg-2)).dragover state swaps border and icon colour to var(--accent)Horizontal control bar — a wrapping flex row of buttons and controls with separator and spacer slots.
When to use: Editor control strips, card action rows, composer footers, and any cluster of related controls that should wrap gracefully when cramped.
Classes: .toolbar, .separator, .spacer
.toolbar is a horizontal control bar: a wrapping flex row of buttons and
controls. It is the public control-row pattern consumed by
.composer and is equally at home wrapping a card's
actions or an editor's formatting controls.
.toolbar — flex row with --vs-xs gap. flex-wrap: wrap is on by default,
so controls reflow onto a new line when the container is narrow.> .separator — a hairline vertical divider (--fg-2) for grouping related
controls. Slot it as a <span class="separator"> between groups.> .spacer — a flex: 1 push-to-end helper. Everything after it is pushed to
the inline-end, e.g. to anchor a primary action on the trailing side.The toolbar carries layout only — gap, wrap, and the two slots. Style its
children with the existing button vocabulary (.button, .minimal, .ghost)
and .icon-button for icon-only controls; icon-only controls must carry an
aria-label. .composer consumes .toolbar directly, adding only its own
inline padding as a context override.