Composed multi-element patterns.
Larger composed patterns built from Graffiti primitives. Navigation, dialogs, forms, and app shells.
Native HTML disclosure element with smooth animations using `<details>` and `<summary>`.
When to use: Expandable sections for FAQ and settings content.
Classes: details, .bordered, .right, .minimal
details::details-content transitions block-size from 0 to auto, unlocked by interpolate-size: allow-keywords at :root so the auto keyword is interpolable. The initial open of an [open] element animates in via @starting-style.› arrow indicator that rotates on openprefers-reduced-motion: keyword interpolation is only enabled when motion is allowed, and the global reduced-motion guard short-circuits the transition duration, so the panel snaps open/closed instead of animating.Use the name attribute to group accordions:
Only one in the group can be open at a time.
Simple breadcrumb navigation with customizable separators.
When to use: Hierarchy path navigation inside app and docs views.
Classes: .breadcrumbs
Default separator is /. Change with --separator:
Other separator ideas: ›, →, », |, ·
Mark the current page with aria-current="page" on the <li>:
This removes the link styling and shows it as plain text.
::before pseudo-elements--fg-5) and brighten on hover (--fg-7)aria-label="Breadcrumb" on the <nav>aria-current="page" on the current page <li><nav>, <ul>, <li> structureCard footer style pagination with previous/next controls and current page state.
When to use: Paginated result navigation with previous and next controls.
Classes: .pagination
.pagination handles layout only (flex, gap, border-top).button.ghost stylesul get fixed 2rem square sizingaria-label="Pagination" on the <nav>aria-current="page" on the active page linkaria-disabled="true" for disabled controlsNative dropdown menu using HTML popover API and CSS anchor positioning.
When to use: Action menus using popover and anchor positioning.
Classes: .dropdown, .dropdown-menu, .dropdown-header, .end
No JavaScript required for open/close. The trigger and menu must be children of the
same .dropdown, and each menu still needs a document-unique id referenced by
its trigger's popovertarget.
.dropdown supplies a locally scoped --dropdown anchor by default. The local
scope lets several dropdowns reuse that internal name without a menu attaching to
another instance. No inline custom property is required.
The legacy --anchor override remains available for integrations that already
assign an explicit dashed-ident:
Normal dropdowns should use the automatic local anchor. If supplied, the override
must start with --; local scoping still keeps it isolated to that dropdown.
.dropdown scopes the default anchor name to its subtree[popovertarget] trigger declares the anchor used by its menupopovertarget on the button points to the menu's idpopover attribute enables native popover behavior.dropdown-menu provides styling and positioningOpening and dismissal require the HTML Popover API. Placement requires CSS anchor
positioning, including anchor-scope. In supporting browsers, the default aligns
the menu's inline-start edge to the trigger and .end aligns their inline-end
edges. In browsers without all of those placement features, the native popover
still opens and its actions remain usable, but it may render at an unanchored
fallback position. Graffiti does not add a JavaScript positioning polyfill.
Vertical navigation for app sidebars with collapsible sections.
When to use: Sectioned app navigation with collapsible groups.
Classes: .sidebar-nav, .sidebar-nav.compact, .sidebar-nav.ghost, .sidebar-nav.minimal, .sidebar-nav.strong-active, .sidebar-nav.primary, .sidebar-nav.success, .sidebar-nav.warning, .sidebar-nav.error, .sidebar-nav.dark, .sidebar-nav.light, .sidebar-nav.contrast, .sub
Uses native <details>/<summary> for expand/collapse.
Icons are automatically sized to 20px (customizable via --sidebar-nav-icon-size).
--sidebar-nav-icon-size - Icon size (default: 20px)--sidebar-nav-indent - Indentation for nested items (default: 1.5rem)Use .compact when a sidebar needs denser rows:
Compact mode keeps focus and hover behavior, while reducing row padding and icon size.
.ghost swaps the row gradient for a transparent fill with a thin border that strengthens on hover and active. Use it when the sidebar should read as part of a surrounding panel rather than a filled control.
.minimal drops the row chrome entirely — no border, no background, no shadow. Hierarchy comes from text color: inactive rows are --fg-6 (inherited from the base row rule), the active row sits at --fg-8, and hover lifts to full --fg.
Compose .strong-active with .minimal or .ghost when the default active brightness isn't pulling enough hierarchy. The active row paints in full --fg and the hover state matches, so the selected row is clearly the brightest thing in the list.
Add .primary, .success, .warning, or .error to re-skin the active row's gradient and text in a theme color. These are shorthands for --sn-color: var(--primary) etc.
You can also set --sn-color inline to any color in scope:
Use .dark, .light, or .contrast when the sidebar should sit on a fixed surface regardless of theme. .dark always renders on a near-black surface, .light on white, .contrast flips against the active color scheme.
.tag placed inside a sidebar row gets tighter padding so badge-style accents (counts, keyboard hints) don't inflate the row height.
Combine with .layout-sidebar.fill for the canonical shell:
Notes:
.layout-sidebar.fill is app-shell-oriented by default (--layout-gap: 0, height: 100dvh).app-shell.app-shell, overflow is not forced there, avoiding double-scrollUse this when navigation should remain visible while main content scrolls.
Popover-driven slide-in panel that anchors to any edge — left, right, top, or bottom.
When to use: Side navigation, mobile bottom sheets, filter panels, and any popover surface that should fly in from a screen edge.
Classes: .drawer, .left, .right, .end, .top, .bottom
.drawer is a single primitive with four edge anchors. It rides on the native [popover] attribute, so opening and closing requires zero JavaScript — use popovertarget on a button.
By default, the drawer anchors to the inline start (the left edge in LTR).
Add .right, .top, or .bottom to re-anchor the drawer to that edge. The slide-in animation, border placement, and sizing follow the edge.
.end is an alias for .right (matches the historical sidebar convention).
| Class | Anchors to | Slide direction | Sizing |
|---|---|---|---|
| (none) | Left | From left | --drawer-inline-size wide, capped at 85vw |
.right | Right | From right | --drawer-inline-size wide, capped at 85vw |
.top | Top | From top | Full width, up to 85dvh |
.bottom | Bottom | From bottom | Full width, up to 85dvh |
Left/right drawers are hard-capped at max-inline-size: 85vw — pushing --drawer-inline-size past that clamps to the cap.
.drawer.bottom is the modern, accessible bottom sheet — full-width slide-up with a scrim and proper focus management courtesy of the popover API.
[popover], so it gets a backdrop, focus trapping, and ESC-to-close for free.translate + @starting-style. No JavaScript required.85dvh so they never block the entire viewport.iOS and Android-friendly CSS patterns for PWAs and native-like web apps.
When to use: App shell, bottom nav, bottom sheet, safe areas.
Classes: .app-shell, .bottom-nav, .bottom-nav.blur, .bottom-sheet, .safe-top, .safe-bottom, .safe-x, .hide-scrollbar, .momentum-scroll
Graffiti provides CSS variables for iOS safe areas (notch, home indicator, status bar):
These inherited tokens use env() to return the user agent's safe area, or 0px
on an ordinary rectangular viewport. For embedded app shells, override them on
the shell's containing element; descendants such as .app-shell, .bottom-nav,
and .bottom-sheet inherit the host-provided insets:
An override replaces the corresponding user-agent value. To add host spacing
while preserving a device inset, include env() in the override, for example
--safe-bottom: calc(env(safe-area-inset-bottom, 0px) + 1rem).
Grid-based container that avoids the iOS URL bar 100vh bug:
Features:
100dvh (dynamic viewport height) to avoid iOS URL bar issuesmin-block-size: 0 on shell and direct regions)Behavior:
.layout-sidebar.fill now acts as an app-shell frame by default (--layout-gap: 0, height: 100dvh).app-shell first/second children auto-scroll on larger layouts.app-shell, overflow is not forced on that child; its main handles scrollingFixed tab bar for mobile apps:
Features:
position: fixed, inset from the bottom + sides, fully rounded (var(--br-xxl)) with var(--shadow-3)--safe-bottom for the home indicatoraria-current="page" or .active class (active color = var(--primary))--fl: -1 labelAdd .blur for a translucent glass effect (70% bg + 20px backdrop blur):
Drawer that slides up from bottom:
Features:
--safe-bottomFor interactive open/close, wrap in <dialog> or use with popover API.
For safe areas to work, include viewport-fit=cover:
Without this, safe area insets may not be reported correctly on iOS.
Full-width site header with navigation.
When to use: Page-level site/app top navigation bar.
Classes: .header, .header.border, .header.sticky, .header.readable
.readable to cap at 1400px)--gap: 1rem between children<ul> styled as horizontal flex list.sticky uses --z-overlay and a solid var(--bg) backgroundSite footer with navigation columns, copyright, and legal links.
When to use: Site footer with grouped navigation and legal links.
Classes: .footer
Uses container queries for responsive behavior.
.footer - Container with container-type: inline-size, removes link underlines (shows on hover).grid.auto - Responsive auto-fit grid for nav columns.footer sets container-type: inline-size so child layouts respond to footer width, not viewport.grid.auto uses auto-fit with --grid-min variable for responsive columns.layout-sidebar and .split automatically stack in narrow containers--grid-min - Minimum column width for .grid.auto (default: 150px, recommended: 120px for footer)The footer uses container queries, not media queries:
.layout-sidebar stacks vertically.grid.auto columns wrap based on --grid-min.split stacks when container is < 500px.box - Adds padding and border.stack - Vertical spacing between children.cluster - Horizontal wrapping layout.split - Space-between horizontal layout.layout-sidebar - Two-column layout (stacks in narrow containers).grid.auto - Auto-fit responsive gridHorizontal swipe-to-reveal component using CSS scroll-snap.
When to use: Swipe-to-reveal row actions for touch interactions.
Classes: .swipe, .stop
Reveal action buttons by swiping left or right.
Action children should be <button> elements — the 200px action width is applied via > button. Non-button actions (<a>, <div>) will not get that width.
User account dropdown combining avatar trigger with dropdown menu.
When to use: Avatar trigger plus account actions dropdown.
Classes: .avatar, .dropdown
Works with both image and initials avatars. Set a unique --anchor on each dropdown so the menu attaches to its avatar trigger.
.dropdown.end - Aligns menu to right edge of avatar.avatar - Circular avatar styling on the button.avatar.bordered - Adds a subtle border around the avatar.dropdown-menu - Menu styling.dropdown-header - User name display in menu.dropdown requires a --anchor inline style (dashed-ident, unique per instance) so the menu can position-anchor to its trigger. See the Dropdown topic for details.
A confirmation dialog pattern using native `<dialog>` with title, message, and action buttons.
When to use: Destructive or important confirmations using native dialog.
Classes: dialog, .close
.close - Circular close button (auto-positions top-right only when a direct child of <dialog>).stack - Vertical layout for dialog content.cluster - Horizontal layout for action buttons.h4 - Heading style without using an actual heading element.primary - Primary action buttoncommandfor/command) - no JavaScript for open/close.stack provides consistent vertical spacing between title, message, and buttons.cluster with justify-content: flex-end aligns buttons to the rightGeneric interactive popover using the native Popover API and CSS anchor positioning.
When to use: Rich revealed content containing links, controls, or forms that would be invalid inside a tooltip.
Classes: .popover-anchor, .popover, .end, .top, .left, .right
.popover-anchor creates a local anchor scope. Its direct popovertarget trigger names that anchor, and a sibling .popover[popover] attaches below it without per-instance positioning styles.
The default placement is block-end aligned to inline-start. Add .end, .top, .left, or .right to the popover. Keep popover="auto" for light dismissal; use manual only when Decks explicitly owns dismissal. A popover may contain interactive content. A tooltip may not.
Activity feeds, step indicators, and progress tracking with status variants and glow effects.
When to use: Progress steps and chronological activity flows.
Classes: .timeline, .steps, .horizontal, .active, .completed, .success, .warning, .error, .info
.timeline and .steps.timeline and .steps are the same primitive — pick the name that matches intent. Use .timeline for chronological event lists (activity feeds, changelogs, history). Use .steps for ordered process flows (onboarding, multi-step forms, checkout).
| Class | Use | Visual |
|---|---|---|
.active | Current step in progress | Bold filled marker with ring |
.completed | Finished step | Green filled marker with glow |
.success | Success status | Green-tinted background with glow |
.warning | Warning status | Yellow-tinted background with glow |
.error | Error status | Red-tinted background with glow |
.info | Info status | Blue-tinted background with glow |
The .marker element can contain:
<span class="marker">1</span><span class="marker"><svg>...</svg></span><span class="marker">✓</span>SVG icons are automatically sized to 1.125rem (18px).
0 0 0 3px spread shadow for colored halo effectShallow notification viewport, item, toast, optional progress, tone, and placement contracts.
When to use: Brief asynchronous feedback that should not interrupt the current task.
Classes: .toast-viewport, .toast-item, .toast, .toast-progress
Decks owns the queue, live-region urgency, timeouts, pause/resume, insertion, and removal. The canonical public authoring surface is .toast-viewport, .toast-item, .toast, and optional .toast-progress. New toast markup stays shallow and class-light.
A persistent toast has no progress indicator and remains visible until the consumer or dismiss control hides it:
The modern visual contract is a neutral, configurable surface with elevation. It has no default border or accent marker. Tone modifiers are .info, .success, .warning, and .error; each sets --toast-tone, which colors optional tracked progress by default without recoloring the surface. Use role="status" for info/success. Reserve role="alert" for urgent warning/error.
A timed semantic toast includes .toast-progress and sets --toast-duration to the same duration as its real auto-dismiss timer:
The progress indicator is decorative and tracks --toast-duration; it does not provide the timer. Add data-paused="true" to .toast-progress whenever the matching dismissal timer is paused. Under reduced motion, the countdown stops and toast visibility and movement transitions become effectively immediate. Omit .toast-progress for persistent or manually dismissed notifications.
Adding popover="manual" gives the viewport native top-layer visibility. Closed popover viewports stay hidden until opened. Placement modifiers are .top-start, .top-center, .top-end, .bottom-start, .bottom-center, and .bottom-end; they use logical insets and safe-area tokens. Omitting popover remains valid for application-controlled stacks.
Consuming component inputs map to Graffiti output rather than requiring a particular framework implementation:
| Consumer concern | Graffiti output |
|---|---|
| Placement | A viewport modifier, or data-inline="start|center|end" with data-block="start|end" on .toast-viewport |
| Tone | .info, .success, .warning, or .error on .toast |
| Progress | Presence or absence of a direct .toast-progress child |
| Duration | --toast-duration on .toast, synchronized with the consumer's dismissal timeout |
| Dismissible | A direct native button[popovertargetaction="hide"] targeting the viewport's id; omit it when dismissal is unavailable |
| Surface/config | Public custom properties applied to the viewport or toast |
Public controls are grouped by purpose:
--toast-width, --toast-offset.--toast-bg, --toast-border, --toast-radius, --toast-shadow, --toast-padding-block, --toast-padding-inline, --toast-gap.--toast-fg, --toast-muted, --toast-font-size, --toast-line-height, --toast-title-weight.--toast-tone, --toast-progress-color, --toast-progress-track, --toast-progress-size, --toast-duration.An optional direct action composes the existing button system rather than introducing toast-owned styling. For example, add <button class="mini minimal" type="button">Undo</button>, or choose another documented button variant when the action needs different emphasis. Do not wrap or stretch the action to the toast width.
.toast-header, .toast-body, .toast-actions, and .toast-dismiss remain compatibility selectors for existing consumers, but new markup does not need them.
A complete login form using Graffiti's base form styles.
When to use: Email/password auth form with native control styling.
Classes: form, .stack
No custom CSS needed.
.stack - Vertical layout with consistent spacing.split - Flexbox with space-between (checkbox left, link right).primary - Primary action button style.error, .success, .warning classes on inputs for validation borders (or rely on :user-invalid / [aria-invalid="true"])<small> placed directly after an input is auto-styled as a caption — colour it inline (style="color: var(--red-6)") or use a .callout.error for prominent messagesMulti-line input with an inline toolbar — model, tool, attach controls alongside the text.
When to use: AI chat surfaces, comment and post composers, email drafts — anywhere a textarea has adjacent affordances. Use plain `.chat-composer` when only a single input + send button is needed.
Classes: .composer, .composer > textarea, .toolbar, .toolbar > .spacer
.composer is a multi-line input with an inline toolbar. It supersedes
.chat-composer when you need model, tool, or attach
controls next to the text input. Also useful for comment and post surfaces —
anywhere a textarea has adjacent affordances.
.composer — column container. Border, --br-l radius, --shadow-2 lift.:focus-within — border switches to --primary, adds a brand-tinted ring.
Pure CSS, no JS.> textarea, > input[type="text"] — borderless inputs that inherit the
composer's surface. resize: none; size with rows..toolbar — the public .toolbar control row, consumed
directly. Children are typically icon buttons, chips, or a .spacer to push
the submit to the trailing end; the composer adds only its own inline padding..toolbar > .spacer — flex: 1 push-to-end helper.Inside .layout-rail, the toolbar wraps automatically at narrow container
widths (<768px). Use the existing .chip density rather than introducing
new mobile-specific classes.
All form inputs are styled automatically. No classes needed on inputs.
When to use: Field rows and form actions.
Classes: .row, .form-option-row, .form-actions
Use .form-option-row on checkbox/radio labels to align controls and text with consistent spacing.
Add .error, .success, or .warning to inputs. These classes set the input's border color; :user-invalid and [aria-invalid="true"] produce the same error styling automatically.
A <small> placed directly after an input is auto-styled as a caption (block display, spacing). Style its color inline (style="color: var(--red-6)") or use a .callout.error if you need a prominent validation message.
Use .row as a field wrapper to group label + control + help text.
Notes:
.row keeps its spacing utility behavior (margin-block)..row becomes a compact field-group wrapper.Use .form-actions for submit/cancel rows.
Behavior:
Right-aligned pane with a tabbed body — artifact viewer, properties inspector, code preview.
When to use: IDE-shaped surfaces — agent artifacts, file previews, properties inspectors, anything that wants a "right rail" alongside the main reading column.
Classes: .workbench-panel, .workbench-panel > header, .workbench-panel > header > .tabs, .workbench-panel > .body
.workbench-panel is a right-aligned content pane with a tabbed header
and a scrolling body. Pairs with .layout-rail.with-workbench
to build IDE-shaped surfaces.
> header — title row with a .tabs group pushed to the trailing end.
Tab activation uses aria-pressed="true" (no aria-selected — there's no
ARIA tab semantics implied; treat each as an independent toggle that the
consumer wires up).> .body — the scrolling content area (flex: 1; overflow: auto).Pair with .layout-rail.with-workbench for a 4-column shell (rail / list /
main / workbench). On mobile (<768px container width), the workbench
collapses with the other rail children.
Board, lane, task card, pointer drag, drop target, selected, and keyboard-drag visual states.
When to use: Moving ordered work items among named workflow stages.
Classes: .kanban-board, .kanban-column, .kanban-column-header, .kanban-card, .kanban-dropzone
The board scrolls horizontally and keeps columns snap-aligned on narrow containers. A column contains .kanban-column-header, a card list, and one or more .kanban-dropzone destinations. Task cards compose .card kanban-card.
Decks toggles dragging / data-dragging, keyboard-dragging / data-keyboard-dragging, row selection, drag-over, and active/invalid dropzone states. Keyboard drag mode must provide grab/cancel/move commands, preserve focus, and announce every successful or rejected move through a polite live region. Pointer drag is an enhancement, never the only path.