Customize

Search 206 classes, tokens & guides from the framework metadata.

Graffiti

github

Elements

Single-purpose UI elements.

These are common, non-complex UI elements that just need a class or two. Forms, cards, buttons, and more.

Buttons

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

Reference Notes
Surface Variants

.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 Utility

.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.

Styling Details
  • All buttons have consistent padding and border-radius
  • Hover, focus, and active states included
  • Disabled state reduces opacity and prevents interaction

Icon Button

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

Reference Notes

.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).

Variants

.icon-button composes with the button variants. Use .mini for a tighter footprint and .ghost / .minimal for low-emphasis toolbar controls.

Classless Auto-Square

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.

Glyph Sizing
  • Any SVG inside a <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.
  • Square padding comes from --icon-button-pad; .mini tightens it.
  • The footprint stays square via aspect-ratio: 1, so it never drifts wider than it is tall regardless of the icon.
Accessibility
  • Always pair an icon-only control with aria-label — the auto-square rule only triggers when the label is present, nudging you toward an accessible name.
  • The inner <svg> should be aria-hidden="true" so the label is the single accessible name.

Floating Action Button

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

Reference Notes

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.

Basic Usage
  • It carries a single icon, so it must always have an aria-label — the same icon-only naming convention .tip and .icon-button rely on.
  • The inner <svg> should be aria-hidden="true" so the label is the one accessible name.
Positioning and Safe Areas
  • 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.
  • Layered at var(--z-sticky) to sit above scrolling content, matching the .bottom-nav stacking convention.
  • Elevation is var(--shadow-4) — one step stronger than .bottom-nav since the FAB floats higher in the stack.
  • Default footprint is --size: 56px (the Material-standard FAB size); override --size for a mini or extended variant.
Composing with Bottom Nav

When a .bottom-nav is present, lift the FAB so it clears the tab bar by overriding its bottom inset:

Motion and Accessibility
  • The hover lift comes from .button and degrades automatically under the global prefers-reduced-motion guard — .fab introduces no animation of its own.
  • Keyboard focus shows the shared focus ring inherited from .button.
  • Because it is a real <button>, it works with zero JavaScript and is fully keyboard and screen-reader operable.

Segmented Control

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

Reference Notes

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.
  • Checked, focus-visible, hover, and disabled states derive from the nested native input. Do not replace the radios with manually synchronized buttons.

Combobox and Listbox

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

Reference Notes

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.

Tag Input

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

Reference Notes

.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.

Chips

Interactive pill-shaped elements for filters, categories, and selections.

When to use: Selectable pills for filters and segmented choices.

Classes: .chip, .selected, .mini

Reference Notes

Use for multi-select interfaces, tag filters, or skill selectors.

With Icons

Icons are automatically sized to 1em.

Styling Details
  • Pill shape with border-radius: var(--br-xxl)
  • Border: var(--border-1)
  • Selected state: primary color background
  • Hover/focus/active states included
  • Fluid typography at --fl: -1 (smaller text)
Chips vs Tags
  • Chips are interactive (clickable, selectable)
  • Tags are for display (category labels, metadata)

Date Picker and Calendar

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

Reference Notes

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.

Tags

Subtle category labels with customizable colors.

When to use: Status or category labels.

Classes: .tag, .success, .warning, .error, .info, .muted

Reference Notes

Use for metadata, categories, or status indicators.

Custom Colors

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

With Icons

Icons are automatically sized to 1em.

Styling Details
  • Soft tinted background derived from --tag-color via OKLCH lightness/chroma adjustments (theme-aware in light and dark)
  • Text color auto-adjusts for light/dark themes
  • Subtle border derived from --tag-color
  • Fluid typography at --fl: -1 (smaller text)
  • Pill shape with border-radius: var(--br-xxl)
  • Interactive tags (<a>/<button>) lift 1px on hover (translate: 0 -1px)
Tags vs Chips
  • Tags are for display (category labels, metadata)
  • Chips are interactive (clickable, selectable)

Rating

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

Reference Notes

.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.

Display Form (Read-Only)

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.

  • Filled stars use --warning (the semantic amber token); the empty track uses --fg-2. Override --rating-on / --rating-off to retint.
  • Adjust scale with --rating-size (defaults to 1.25em).
Input Form (Interactive)

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.

  • Each <label> carries an aria-label ("3 stars") so the choice is announced.
  • The radios stay focusable (visually hidden, not display: none); the active label gets the standard focus ring on :focus-visible.
  • Hover preview and the checked transition are short-circuited automatically under prefers-reduced-motion via the global motion guard.
Accessibility Notes
  • Display form: role="img" + aria-label is the single source of truth for the value — keep the label in sync with --rating.
  • Input form: the fieldset + legend group the radios; submit the chosen value like any other radio group.

Reactions

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

Reference Notes

.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.

Anatomy

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.
Behaviour & fallback

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.

Accessibility
  • Always give the <select> an aria-label (or an associated <label>) — the reactions themselves are emoji, so the control needs its own name.
  • Keep a .visually-hidden text label inside every <option> so each reaction is announced by name in both the styled bar and the native fallback.
  • The trigger and options take the standard focus ring on :focus-visible; the open/close entrance is short-circuited under prefers-reduced-motion by the global motion guard.
Tuning
  • --reaction-size (default 1.35rem) sets the trigger glyph size; the picker scales its option emoji off the same value.
  • The bar opens centred above the trigger (position-area: block-start + justify-self: anchor-center) and flips below via position-try-fallbacks: flip-block when there isn't room.

Avatar

Circular avatar for user images or initials.

When to use: User photos or initials with size variants.

Classes: .avatar, .xs, .s, .l, .xl, .bordered

Reference Notes
Styling Details
  • Circular shape (border-radius: 50%)
  • Images use object-fit: cover
  • Initials have subtle background (--fg-1)
  • Text scales with avatar size

Boxes

Container styles for cards and content blocks.

When to use: Container surface styles and quick panel variants.

Classes: .box, .glow, .semi-gloss, .ghost, .invisible

Reference Notes
Styling Details
  • .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 structure

Callouts

Informational 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

Reference Notes
With Icon

The first direct <svg> child is pulled into the gutter and colored with --callout-accent:

Multiple Elements

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.

CSS Variables
  • --callout-tint - Background color (applies to .fill only)
  • --callout-accent - Icon color

Card

A 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

Reference Notes
Basic Card

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.

With Media

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.

Notes
  • .card is the padded surface. Don't wrap content in an extra <div>.
  • Children stack with 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.
  • Skip headers and footers for simple cards. Most cards don't need them.

Bubble

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

Reference Notes

.bubble is a rounded chat container with configurable padding, max-width, and child flow spacing.

Chat Layout Helpers

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 space
In-Flight Variants

Two 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).
Flowing Thread (Bubble-less)

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:

CSS Variables
  • --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 elements

Icon Rail

Narrow 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

Reference Notes

.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.

Anatomy
  • .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.
  • > .spacerflex: 1 filler to push the next children to the bottom.
Composition

Sits left of a .layout-sidebar, sub-sidebar (.chat-list or similar), or inside .layout-rail:

CSS Variables
  • --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

Reference Notes
Styling Details
  • display: flex with horizontal overflow
  • scroll-snap-type: x mandatory for snapping
  • Children have scroll-snap-align: start
  • Thin scrollbar (scrollbar-width: thin)
  • Default gap of 1rem
How It Works
  • Drag, swipe, or scroll horizontally
  • Items snap to start position
  • Works with mouse, touch, trackpad, and keyboard
  • No JavaScript needed for basic functionality

Log Card

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

Reference Notes

.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.

Anatomy
  • > 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.
  • Any other body content (paragraphs, summaries) also works — <pre> is just the most common.
Composition

Sits inside a .chat-message for AI tool calls, or anywhere a single-row-with-payload affordance is needed:

Reel

Vertical scrolling with CSS scroll-snap. Like carousel but vertical.

When to use: Vertical scroll-snap list or feed.

Classes: .reel

Reference Notes
CSS Variables
  • --reel-height - Container height (default: 80vh)
  • --gap - Gap between panels (default: 1rem)
Styling Details
  • display: flex with flex-direction: column
  • scroll-snap-type: y mandatory for snapping
  • Children have scroll-snap-align: start
  • Vertical overflow with thin scrollbar
How It Works
  • Scroll vertically to navigate panels
  • Panels snap to top
  • Works with mouse wheel, touch, and keyboard

Tables

Responsive table wrapper with clean data table styling.

When to use: Responsive table wrapper and default table styling.

Classes: .table, .table.zebra, .table.sticky

Reference Notes

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:

  • Pure CSS, zero JavaScript — position: sticky does all the work.
  • The header <th> cells get an opaque var(--bg) so scrolling rows do not show through.
  • .sticky composes with .zebra on the same wrapper.
  • Override the scroll height with --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.

Why the Wrapper?

The .table wrapper provides:

  • Horizontal scrolling on small screens
  • Border and border-radius on the container
  • Proper overflow handling
Styling Details
  • Tables are 100% width with collapsed borders
  • Headers have bottom border separator
  • Cells have consistent padding
  • Last row has no bottom border
  • Wrapper has overflow-x: auto for responsiveness
CSS Variables
  • --table-border - Custom border-radius for wrapper

Data Table

Complete 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

Reference Notes

.data-table composes a .data-table-toolbar with the existing .table > table contract. Decks owns TanStack state, callbacks, keyboard behavior, and server coordination.

  • Put aria-sort on the <th> and a .data-table-sort button inside it.
  • Put aria-selected="true" on selected rows. Selection checkboxes retain value-specific labels.
  • Use .data-table-actions on an action cell or toolbar group.
  • Use a spanning .data-table-empty cell for filtered zero-results.
  • Use .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.

Async and Empty States

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

Reference Notes

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.

Dialog

Native HTML `<dialog>` element with open/close animations.

When to use: Native modal flows and confirmations.

Classes: dialog, .close

Reference Notes

No JavaScript required when using HTML invokers.

How It Works
  • commandfor points to the dialog's id
  • command="show-modal" opens the dialog as a modal
  • command="close" closes the dialog
  • No JavaScript needed for basic open/close
Close Button

The .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.

Styling Details
  • max-width: 40ch - Character-based width for good proportions
  • Dark backdrop overlay
  • Smooth scale/opacity animation on open/close
  • Works in light and dark themes automatically

Tabs

Pure 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

Reference Notes

No JavaScript required.

How It Works
  • The 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)
Important Notes
  1. Unique names: Each tab group needs a unique name attribute
  2. Open one by default: Add open to the tab that should be visible initially
  3. Sequential --n values: Must match the visual order (1, 2, 3, etc.)
  4. Match --tab-count: If you have more than 3 tabs, set --tab-count to match
Styling Details
  • Uses CSS Grid with Subgrid for alignment
  • Smooth opacity transitions with @starting-style
  • Works in light and dark modes
  • Keyboard accessible (native details/summary behavior)
  • Active tab has underline indicator (default), card connection (boxed), or sliding thumb segment (pill)

Tooltip

Non-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

Reference Notes

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.

Accessible Rich Tooltip

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:

Decorative Pseudo Tooltip

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.

Activation and Dismissal
  • Pointer hover and trigger focus reveal the same text; pointer exit and focus exit hide it.
  • Focus stays on the trigger. The tooltip itself is never focusable and has pointer-events: none.
  • Decks may render controlled state with .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.
  • Essential instructions must also appear persistently in the interface. A tooltip is supplemental only.
  • Graffiti's zero-JavaScript CSS cannot make Escape clear :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.
Styling Details
  • Uses CSS anchor positioning (anchor-scope, position-anchor, position-area) with .tooltip-trigger as the canonical anchor
  • Background: var(--bg)
  • Border: var(--border-1)
  • Shadow: var(--shadow-3)
  • Max width: 30ch (wraps longer text)
  • Shows on :hover and :focus-visible / :focus-within
  • Uses visibility: hidden while closed so rich tooltip nodes do not create stray reading-order content
  • Smooth opacity transition (var(--d-fast) / var(--ease-smooth))
Migration from 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.

List Navigation

Navigation list with clickable rows for settings pages, menus, and navigation indexes.

When to use: Grouped list-style navigation rows.

Classes: .list-nav

Reference Notes

Each item is a pill-shaped card with subtle shadow.

Use Cases
  • Settings pages
  • Mobile app menus
  • Feature indexes
  • Dashboard navigation
  • Account/profile menus
Item Structure

Items are direct <a> or <button> children of .list-nav. No additional classes needed.

Each item can contain:

  1. <svg> - Icon on the left (sized to 1.25em)
  2. Text - Title text
  3. <small> - Optional description (muted, smaller text)
Styling

Each item automatically gets:

  • Pill radius - var(--br-xxl) rounded corners
  • Subtle shadow - var(--shadow-2) for elevated card appearance
  • Background - Uses var(--bg) for proper theming
  • Gap - var(--pad-m) spacing between items
  • Dark mode - Items pick up a var(--border-1) border for definition
States
  • Default: Pill-shaped card with subtle shadow
  • Hover: Background highlight, icon brightens
  • Focus: Focus ring with inset offset
  • Active: Slightly darker background
  • Disabled: 65% opacity, no pointer events
Differences from Sidebar Nav
FeatureList NavSidebar Nav
Item styleIndividual cards with shadowFlat items in a list
DescriptionSupported via <small>Not supported
Use caseStandalone navigation rowsSticky sidebar menus
NestingNot supportedSupports details/summary
Visual weightHigher (cards with shadow)Lower (compact)
Accessibility
  • Use semantic <nav> container
  • Use <a> for navigation links, <button> for actions
  • Disabled buttons use disabled attribute
  • Links can use aria-disabled="true" when needed
  • Focus states are clearly visible

Toggle Switch

Accessible toggle/switch input using native checkbox.

When to use: Boolean settings with checkbox semantics.

Classes: .toggle, .compact

Reference Notes

<span id="forms">

CSS Variables
  • --toggle-color - Color when checked (default: var(--primary))
  • --toggle-width - Track width
  • --toggle-height - Track height
Accessibility

The toggle uses a native checkbox, so it:

  • Works with keyboard (Space to toggle)
  • Announces state to screen readers
  • Respects prefers-reduced-motion

Input Group

Input field with connected button.

When to use: Input plus attached action button patterns.

Classes: .input-group, .affix, .input-group.stack-mobile

Reference Notes

Use for copy-to-clipboard, search with button, URL sharing, or any input that needs an action.

Static Affixes

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.

  • Use a leading .affix as the first child for symbols like $ or .
  • Use a trailing .affix as the last child for units like kg, %, or USD.
  • You can combine both around a single input.
  • Pair with type="number" plus inputmode so mobile keyboards match the expected entry.
Stack on Mobile

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.

Styling Details
  • Input stretches to fill available space
  • Button stays sized to content
  • Connected with no gap, shared border-radius
  • Works with all button variants (.primary, .ghost, etc.)

Search input with icon positioned inside.

When to use: Search field with icon and compact action behavior.

Classes: .search

Reference Notes
Icon Source

Get icons from Phosphor Icons. The magnifying glass icon shown above is the "MagnifyingGlass" icon.

Styling Details
  • Icon is absolutely positioned, vertically centered
  • Input has left padding to accommodate the icon
  • Icon is muted (var(--fg-3)) so the user's typed text reads as the primary content

File Dropzone

Drag-and-drop file upload zone with click-to-upload fallback.

When to use: Drag-and-drop upload zones with native fallback.

Classes: .dropzone, .dragover

Reference Notes
Icon Source

Get icons from Phosphor Icons. The upload icon shown above is the "UploadSimple" icon.

Styling Details
  • Padding: var(--pad-xxxl), radius: var(--br-l) (hardcoded; theme by editing the source or wrapping)
  • Dashed border (2px dashed var(--fg-2))
  • Centered flex layout for icon and text
  • File input is transparent but covers the entire area, preserving its native keyboard and pointer behavior
  • Click anywhere to trigger file picker
  • Keyboard focus on the file input is shown on the visible dropzone surface
  • Disabled file inputs dim the dropzone and suppress hover and dragover treatments
  • .dragover state swaps border and icon colour to var(--accent)

Toolbar

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

Reference Notes

.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.

Anatomy
  • .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.
Composition

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.