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
@starting-style and allow-discrete› arrow indicator that rotates on openUse 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.
Each dropdown needs a unique --anchor value set on .dropdown. The menu uses CSS anchor positioning to attach to its trigger, and --anchor is the dashed-ident that ties them together. Without it, the popover falls back to default centered placement.
The value must start with -- and be unique per dropdown on the page.
--anchor on .dropdown declares the anchor name; the menu reads it as position-anchorpopovertarget on the button points to the menu's idpopover attribute enables native popover behavior.dropdown-menu provides styling and positioningVertical 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 use env() which returns the actual safe area on iOS/Android, or 0px on desktop.
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 rightActivity 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 effectA 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 messagesAll 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: