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[aria-label]
.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.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)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 wrapperNative 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-styleHover tooltips using CSS anchor positioning. No JavaScript required.
When to use: Popover-based tooltips with CSS anchor positioning.
Classes: .tip, .tooltip, .bottom, .left, .right
Two patterns share the same visuals — pick by content shape.
.tip (Plain Text via aria-label)Apply .tip directly to an icon button (or any focusable element) and put the label text on aria-label. The same string is the screen-reader accessible name and the visible tooltip — one source of truth, no duplication.
Position modifiers go on the same element:
This pattern is best for icon-only buttons where the tooltip text is the accessible name. The pseudo-element reads attr(aria-label), so it can only render plain text — no markup, links, or nested elements. For those, use the rich pattern below.
Don't combine
.tipwith visible text content.<button class="tip" aria-label="Save to draft">Save</button>would override the visible "Save" label with "Save to draft" for screen readers — that fails WCAG 2.5.3 (Label in Name). When the element already has a visible label and the tooltip is a supplemental description, use the rich pattern below witharia-describedbypointing at the tip.
anchor-scope, position-anchor, position-area)var(--bg)var(--border-1)var(--shadow-3)30ch (wraps longer text):hover and :focus-visible / :focus-withinvar(--d-fast) / var(--ease-smooth))| Use case | Pattern |
|---|---|
| Icon-only button, tip text = accessible name | <button class="tip" aria-label="…"> |
| Element with visible label, tip is supplemental | .tooltip wrapper + child .tip + aria-describedby |
| Tip body needs links, icons, or multi-line markup | .tooltip wrapper + child .tip |
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, .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, .toolbar > .separator, .toolbar > .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.