Design System

Design System

Single source of truth for every visual primitive in this app. Reuse what's here. Propose additions before building one-offs.

Colors

Ten semantic tokens, each defined for light and dark mode. Use the token names — never raw hex. Each swatch shows the value for the mode you're currently viewing; both hex codes are listed below the swatch and click-to-copy.

Surfaces

page
bg-page
surface
bg-surface
hairline
border-hairline

Text

ink-body
text-ink-body
ink-display
text-ink-display
ink-muted
text-ink-muted

Splash

accent
bg-accent / text-accent
accent-faded
bg-accent-faded
accent-display
bg-accent-display / text-accent-display
signal
bg-signal / text-signal
signal-faded
bg-signal-faded
signal-display
bg-signal-display / text-signal-display
danger
bg-danger / text-danger
danger-faded
bg-danger-faded
danger-display
bg-danger-display / text-danger-display

When to use

  • Always reach for a token first.
  • Use page / surface for layered backgrounds.
  • Use ink-display for headlines and emphasized content.

When not to use

  • Never use raw hex values (#1f2937) in components.
  • Don't introduce new color names ad-hoc — extend the system instead.

Sample code

<div className="bg-page text-ink-body">
  <div className="bg-surface border border-hairline">
    <h2 className="text-ink-display">Headline</h2>
    <p className="text-ink-muted">Quiet supporting copy.</p>
    <button className="bg-accent text-page">Primary action</button>
  </div>
</div>

Typography

Two families. Headlines use Inter, body copy uses DM Sans. Both load from Google Fonts via <link> tags in the app's HTML <head>.

Display — Inter
The quick brown fox jumps over the lazy dog
Body — DM Sans
The quick brown fox jumps over the lazy dog. 0123456789. The five boxing wizards jump quickly.

When to use

  • Use font-display for headings only. Headings already inherit it via base styles.
  • Use font-sans for everything else — also already the default.

When not to use

  • Don't import additional font families — propose adding to the system instead.
  • Don't override font-family inline.

Sample code

// Set in design-system.css via @theme:
//   --font-display: 'Stack Sans Text', ui-sans-serif, system-ui, sans-serif;
//   --font-sans: 'DM Sans', ui-sans-serif, system-ui, sans-serif;

<h1 className="font-display">Display headline</h1>
<p className="font-sans">Body paragraph.</p>

Shells

The outermost page frame. A shell is a full-bleed bg-page container with the main-navigation rail on the left and a content area on the right. The rail toggles between collapsed (w-14) and expanded (w-56) via a chevron button (state persisted to localStorage), and is hidden entirely below the lg breakpoint in favor of a fixed hamburger and slide-in drawer. Sub-navigation lives inside page headers as horizontal tabs (not in the shell).

When to use

  • Every authenticated screen.
  • Pair with a Page header at the top of the main content area.

When not to use

  • Marketing/landing pages — those use a dedicated marketing shell with a horizontal top nav.
  • Modals/sheets — they layer on top of the shell, not replace it.

Sample code

<div className="flex min-h-screen bg-page text-ink-body">
  {/* Main navigation rail — see Main navigation section. Hidden below lg;
     a hamburger fixed to the top-right opens the mobile drawer instead. */}
  <MainNav />
  <main className="min-w-0 flex-1 px-6 py-8 sm:px-10">
    <div className="mx-auto max-w-4xl">
      {/* Page header (optional with sub-nav tabs) → page content */}
    </div>
  </main>
</div>

Options & variations

  • Rail-only: just the main-nav rail and a content column. Default for app screens.
  • With sub-nav: page header inside main appends sub-navigation tabs — see Page headers "with tabs" variant. No second sidebar needed.
  • Content max-width: wrap children in mx-auto max-w-4xl so long-form content stays readable. The shell itself is full-bleed; the constraint lives on the inner container.
  • Mobile: hide the rail with hidden lg:flex and render a hamburger fixed to the top-right of the viewport (fixed right-3 top-3 z-30) that opens the same nav as a slide-in drawer overlay.

Page headers

The titled banner at the top of a page's main content area. Holds the page title, optional supporting copy, and a flexible right-side slot for actions. Optionally appends sub-navigation tabs below the title row to produce a two-line header.

Basic — with right-side actions

Projects

Everything your team is working on.

With search in the right slot

Members

Manage who has access to this workspace.

Single-item view — title with settings dropdown (the standard layout for viewing one item from a collection)

Onboarding redesign

With sub-navigation tabs (two horizontal lines)

Acme Inc.

Workspace settings and billing.

When to use

  • Every primary page inside the shell.
  • Right-side slot accepts buttons, icons, search inputs, links — use it for primary and secondary page actions.
  • Use the "with tabs" variant when the page has sub-views (settings, profile, project sections).

When not to use

  • Dialog/sheet contents — use DialogTitle instead.
  • Dense list views with tightly-packed filters — use a thinner toolbar pattern.

Sample code

{/* Basic — title row only */}
<div className="border-b border-hairline pb-6">
  <div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
    <div>
      <h1>Projects</h1>
      <p className="mt-1">Everything your team is working on.</p>
    </div>

    {/* Right-side slot — buttons, icons, search, links */}
    <div className="flex items-center gap-2">
      <button type="button" className="inline-flex h-9 w-9 items-center justify-center rounded-md border border-hairline text-ink-body hover:bg-surface" aria-label="Notifications">
        <Bell className="h-4 w-4" />
      </button>
      <Button variant="secondary">Filter</Button>
      <Button>New project</Button>
    </div>
  </div>
</div>

{/* Single-item view — page title with the standard item settings dropdown
    in the right slot. The Ellipsis trigger uses h-6 w-6 here (larger
    than the h-4 w-4 used in listing rows). */}
<div className="border-b border-hairline pb-6">
  <div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
    <div className="min-w-0 flex-1">
      <h1>Onboarding redesign</h1>
    </div>
    <div className="flex items-center gap-2">
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <button
            type="button"
            aria-label="Settings"
            className="inline-flex cursor-pointer items-center justify-center text-ink-muted transition-colors hover:text-ink-body focus:outline-none focus-visible:text-ink-body"
          >
            <Ellipsis className="h-6 w-6" strokeWidth={1.5} />
          </button>
        </DropdownMenuTrigger>
        <DropdownMenuContent align="end">
          <DropdownMenuItem>Deactivate</DropdownMenuItem>
          <DropdownMenuItem destructive>Delete</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
  </div>
</div>

{/* With tabs — appends sub-navigation, results in two horizontal lines */}
<div>
  <div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between border-b border-hairline pb-6">
    <div>
      <h1>Acme Inc.</h1>
      <p className="mt-1">Workspace settings and billing.</p>
    </div>
    <div className="flex items-center gap-2">
      <Button>Save</Button>
    </div>
  </div>
  <nav className="flex items-end justify-between gap-4 border-b border-hairline">
    <div className="flex items-center gap-6">
      <a href="#" className="-mb-px cursor-pointer border-b-2 border-accent px-1 py-3 text-sm font-medium text-accent-display no-underline">Overview</a>
      <a href="#" className="-mb-px cursor-pointer border-b-2 border-transparent px-1 py-3 text-sm text-ink-body no-underline hover:text-ink-display">Members</a>
      <a href="#" className="-mb-px cursor-pointer border-b-2 border-transparent px-1 py-3 text-sm text-ink-body no-underline hover:text-ink-display">Billing</a>
    </div>
    <div className="flex items-center gap-3 pb-2">
      <a href="#" className="text-sm text-ink-muted no-underline hover:text-ink-display">View audit log</a>
    </div>
  </nav>
</div>

Options & variations

  • Right-side slot: a flex row that accepts any mix of buttons, icon buttons, links, badges, or search inputs. Place primary CTA at the far right; secondary actions to its left.
  • With breadcrumb: prepend a small breadcrumb row above the title.
  • With tabs: append the Sub navigation pattern below the title row's border-b — the tabs row gets its own border-b, producing two horizontal lines with the active tab's underline merging into the lower one.
  • Single-item view: when viewing one item from a collection, the right slot should hold our standard settings dropdown — see Dropdown menu. Use h-6 w-6 on the Ellipsis here (one size larger than in listing rows) to match the page header's heavier visual weight.

Body content

Wrap any long-form prose (articles, marketing copy, doc pages) in.body-content to get consistent vertical rhythm (space-y-6) and breathing room above sub-headings. We do not use Tailwind's typography plugin.

Section heading

Lead paragraph introducing the section. The body font and base type sizes come from the global stylesheet.

A second paragraph shows the consistent vertical rhythm given by .body-content > * + * with margin-top: 1.5rem.

Sub-section

Each h2, h3, or h4 inside.body-content gets pt-4 unless it's the first child.

  • Lists inherit base styles globally.
  • No extra Tailwind utility classes are needed.
And blockquotes look the same wherever they appear.

When to use

  • Articles, blog posts, documentation pages.
  • Anywhere you have a sequence of paragraphs and headings.

When not to use

  • UI chrome — buttons, forms, navs, cards.
  • Tightly-spaced layouts where you control gaps explicitly.

Sample code

<article className="body-content">
  <h2>Section heading</h2>
  <p>Lead paragraph introducing the section.</p>
  <p>Continued prose with consistent vertical rhythm.</p>
  <h3>Sub-section</h3>
  <p>Each h2/h3/h4 inside .body-content gets pt-4 unless it's the first child.</p>
  <ul>
    <li>List items use the base styles defined globally.</li>
    <li>No extra utility classes needed.</li>
  </ul>
  <blockquote>And blockquotes look the same wherever they appear.</blockquote>
</article>

Options & variations

  • .body-content applies vertical spacing only — no font or color overrides. Type comes from base styles.
  • Custom widths are fine: pair with max-w-prose or your own constraint.

Footers

The bottom edge of a shell. Quiet, low-contrast. Holds the copyright, legal links, and (sparingly) secondary navigation.

© 2026 Acme

When to use

  • Public-facing or marketing pages.
  • Authenticated screens that have natural scroll endings.

When not to use

  • Dense app screens (dashboards, editors) — they don't need a footer.
  • Modals.

Sample code

<footer className="border-t border-hairline bg-page">
  <div className="mx-auto flex max-w-7xl flex-col gap-2 px-4 py-6 text-sm text-ink-muted sm:flex-row sm:items-center sm:justify-between">
    <span>© 2026 Acme</span>
    <nav className="flex gap-4">
      <a href="/privacy" className="no-underline hover:text-ink-display">Privacy</a>
      <a href="/terms" className="no-underline hover:text-ink-display">Terms</a>
    </nav>
  </div>
</footer>

Iconography

Icons come from lucide-react. Default size is h-4 w-4 for inline use; h-5 w-5 in buttons; h-6 w-6 for standalone visual anchors.

Activity
ArrowRight
Bell
Check
ChevronRight
Folder
Plus
Search
Settings
Trash2
User
X

When to use

  • Reinforce a button's label or status.
  • Visually anchor empty states and section headers.

When not to use

  • Decoratively, with no semantic value.
  • Replacing a label entirely (icon-only buttons need an aria-label).

Sample code

import { Plus } from "lucide-react";

<Plus className="h-4 w-4" />

Options & variations

  • Pick from the full lucide set: https://lucide.dev/icons
  • Use text-ink-muted for icons that recede; text-accent when emphasizing.
  • Always pair icon-only buttons with aria-label.

Buttons

The <Button> primitive supports six variants and four sizes. Use asChild to render as a different element (e.g. an <a>) while keeping the styling.

When to use

  • Any clickable action — submit, save, navigate, open modal.
  • One primary per scope; secondary/ghost for the rest.

When not to use

  • For inline links inside prose — use <a>.
  • Three or more primary buttons in the same view — collapse to one.

Sample code

import { Button } from "@/components/ui/button";

<Button>Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="soft">Soft</Button>
<Button variant="danger">Danger</Button>
<Button variant="link">Link</Button>

<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<Button size="icon" aria-label="Add"><Plus className="h-4 w-4" /></Button>

Options & variations

  • variant: primary (default) | secondary | ghost | soft | danger | link
  • size: sm | md (default) | lg | icon
  • asChild: render as a Slot — pass a single child element to inherit the styles.

Button dropdown

A split button — a primary action on the left and a chevron end-cap on the right that opens a dropdown of related secondary actions. The two halves render as one shape with a thin currentColor-tinted divider between them, so it works on any variant (and on custom backgrounds set via className). The menu uses the same dropdown primitive documented below.

When to use

  • One action is the obvious next step but a small set of related secondary actions belong in the same spot.
  • Page-header action boxes where the primary action lives next to "park / discard / reset"-style alternates.

When not to use

  • The actions don't share intent — use separate buttons (or a plain DropdownMenu if none of them is the obvious primary).
  • There's only the main action — use a plain Button.
  • The menu is a list of navigation destinations rather than secondary actions — use a plain DropdownMenu.

Sample code

import { ButtonDropdown } from "@/components/ui/button-dropdown";
import {
  DropdownMenuItem,
  DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { ArrowRight, Pause, Trash2 } from "lucide-react";

<ButtonDropdown
  action={<>Greenlight <ArrowRight className="h-4 w-4" /></>}
  onClick={() => doPrimary()}
>
  <DropdownMenuItem onSelect={() => doPark()}>
    <Pause /> Park
  </DropdownMenuItem>
  <DropdownMenuSeparator />
  <DropdownMenuItem destructive onSelect={() => doDiscard()}>
    <Trash2 /> Discard
  </DropdownMenuItem>
</ButtonDropdown>

<ButtonDropdown
  variant="secondary"
  action="Duplicate"
  onClick={() => doDuplicate()}
>
  <DropdownMenuItem onSelect={() => doDuplicateAs()}>
    Duplicate as draft
  </DropdownMenuItem>
  <DropdownMenuItem onSelect={() => doExport()}>
    Export
  </DropdownMenuItem>
</ButtonDropdown>

Options & variations

  • action: content of the left (primary) button. String or any ReactNode.
  • onClick: handler for the primary action. The toggle (chevron) opens the menu and is independent.
  • variant: any Button variant — primary (default), secondary, ghost, soft, danger, link.
  • size: sm | md (default) | lg.
  • className / toggleClassName: extra classes for each half. Useful for custom-colored buttons (e.g. stage-color buttons in this app). toggleClassName defaults to mirror className.
  • menuAlign: start | center | end (default end) — forwarded to DropdownMenuContent.
  • toggleAriaLabel: aria-label for the chevron half. Default "More actions".
  • Children: pass DropdownMenuItem / DropdownMenuSeparator children — the standard dropdown-menu primitives.

Forms

Forms compose <Input>, <Select>,<Checkbox>, <Radio>, <RichTextField> (milkdown), native HTML labels, helper text, and <Button>. Vertical spacing between fields uses space-y-4; spacing inside a field uses space-y-2.

We'll only use this for account notifications.

Plan

When to use

  • All data-entry surfaces.
  • Wrap fields with their own <label> for accessibility.
  • Use <fieldset> + <legend> around radio groups.

When not to use

  • Inline filters in toolbars — use compact controls instead.
  • Single-button calls to action — those don't need a form wrapper unless they POST.

Sample code

import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Radio, RadioGroup } from "@/components/ui/radio";
import { Select } from "@/components/ui/select";
import { RichTextField } from "@/components/ui/rich-text-field";

<form className="space-y-4">
  {/* Text input */}
  <div className="space-y-2">
    <label htmlFor="email">Email</label>
    <Input id="email" type="email" placeholder="you@example.com" />
    <p className="text-xs text-ink-muted">We'll only use this for account notifications.</p>
  </div>

  {/* Select */}
  <div className="space-y-2">
    <label htmlFor="country">Country</label>
    <Select id="country" defaultValue="">
      <option value="" disabled>Choose one…</option>
      <option value="us">United States</option>
      <option value="ca">Canada</option>
      <option value="uk">United Kingdom</option>
    </Select>
  </div>

  {/* Radio group */}
  <fieldset className="space-y-2">
    <legend>Plan</legend>
    <RadioGroup>
      <label className="flex items-center gap-2 text-sm font-normal text-ink-body">
        <Radio name="plan" value="free" defaultChecked />
        Free
      </label>
      <label className="flex items-center gap-2 text-sm font-normal text-ink-body">
        <Radio name="plan" value="pro" />
        Pro
      </label>
      <label className="flex items-center gap-2 text-sm font-normal text-ink-body">
        <Radio name="plan" value="team" />
        Team
      </label>
    </RadioGroup>
  </fieldset>

  {/* Checkbox */}
  <label className="flex items-start gap-2 text-sm font-normal text-ink-body">
    <Checkbox id="newsletter" defaultChecked className="mt-0.5" />
    <span>
      <span className="font-medium text-ink-display">Send me product updates</span>
      <span className="block text-xs text-ink-muted">Roughly one email per month.</span>
    </span>
  </label>

  {/* Rich text (milkdown) */}
  <div className="space-y-2">
    <label htmlFor="bio">Bio</label>
    <RichTextField placeholder="Tell us about yourself…" />
  </div>

  <Button type="submit">Save</Button>
</form>

Options & variations

  • Use HTML <label htmlFor> with the field's id for accessibility.
  • Helper text uses text-xs text-ink-muted directly under the field.
  • Error states: render a text-xs text-danger-display message in the same slot as helper text and add aria-invalid to the field.
  • Radio groups: wrap in <fieldset> + <legend> and share a name across all <Radio> inputs.
  • Selects: use a disabled empty <option> as a placeholder when no default makes sense.
  • Rich text: <RichTextField> wraps milkdown's Crepe — emits markdown via onChange. Requires @milkdown/crepe.

Badges

Small inline tag/badge for status or category. Five tones cover most needs. Always use the primitive — never reach for ad-hoc pill markup.

DraftNewWarningArchivedPro

When to use

  • Status indicators (Draft, Published, Archived).
  • Category tags on a list item.
  • Plan or tier badges (Pro, Free).

When not to use

  • For form field labels — use a real <label>.
  • For navigation — use a button or anchor.

Sample code

import { Badge } from "@/components/ui/badge";

<Badge tone="neutral">Draft</Badge>
<Badge tone="accent">New</Badge>
<Badge tone="signal">Warning</Badge>
<Badge tone="muted">Archived</Badge>
<Badge tone="solid">Pro</Badge>

Options & variations

  • tone: neutral (default) | accent | signal | muted | solid
  • Pair with a small lucide icon as the first child for richer status badges.

Toggle buttons

Pill-shaped buttons with an on/off state. Use them as a small, inline filter or segmented control where one or more options can be active. Built as a base class so any <button> can adopt the look — no React primitive required.

When to use

  • Filter chips above a list ("All / Active / Archived").
  • Small segmented choices inside a toolbar.
  • Multi-select tag pickers where each pill toggles independently.

When not to use

  • For primary actions — use <Button>.
  • For yes/no settings — use a switch or checkbox.
  • For navigation — use anchors styled as tabs.

Sample code

// Class-based: pair .toggle-button with .toggle-button-on for the active state
<button type="button" className="toggle-button toggle-button-on">All</button>
<button type="button" className="toggle-button">Active</button>
<button type="button" className="toggle-button">Archived</button>

// Or drive the on state from aria-pressed (preferred for screen readers)
<button type="button" className="toggle-button" aria-pressed={true}>All</button>
<button type="button" className="toggle-button" aria-pressed={false}>Active</button>
<button type="button" className="toggle-button" aria-pressed={false}>Archived</button>

Options & variations

  • .toggle-button — base pill (off state). Renders on any <button>.
  • .toggle-button-on or aria-pressed="true" — active state. Both selectors are wired up; prefer aria-pressed for assistive tech.
  • Pair with a small lucide icon as the first child for icon+label pills.
  • Disabled state is handled automatically via disabled.

Listings

A vertical list of selectable rows — the workhorse of dashboards and resource indexes. Each row composes an icon, a title, supporting metadata, an optional Badge, a chevron affordance, and (for items in a collection) a standard settings dropdown.

  • Onboarding redesign
    Updated 2h ago · Marie
    Active
  • Q2 marketing rollout
    Updated yesterday · Jordan
    Active
  • API v2 migration
    Updated 3d ago · Priya
    Draft
  • Mobile launch checklist
    Archived last week
    Archived

When to use

  • Lists of resources (projects, files, members).
  • Search results, recent activity feeds.

When not to use

  • Dense, multi-column tabular data — use a table.
  • Inline option lists in dropdowns — use a menu primitive.

Sample code

<ul className="divide-y divide-hairline overflow-hidden rounded-md border border-hairline bg-page">
  {items.map((item) => (
    <li key={item.id}>
      <div className="flex items-center gap-3 px-4 py-3 hover:bg-surface">
        <a href={item.href} className="flex min-w-0 flex-1 items-center gap-3 no-underline">
          <Folder className="h-4 w-4 text-ink-muted" />
          <div className="min-w-0 flex-1">
            <div className="truncate text-sm font-medium text-ink-display">{item.title}</div>
            <div className="truncate text-xs text-ink-muted">{item.subtitle}</div>
          </div>
          <Badge tone="muted">{item.status}</Badge>
        </a>
        {/* Standard item settings dropdown — see Dropdown menu section. */}
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <button
              type="button"
              aria-label="Settings"
              className="inline-flex cursor-pointer items-center justify-center text-ink-muted transition-colors hover:text-ink-body focus:outline-none focus-visible:text-ink-body"
            >
              <Ellipsis className="h-4 w-4" strokeWidth={1.5} />
            </button>
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end">
            <DropdownMenuItem>Deactivate</DropdownMenuItem>
            <DropdownMenuItem destructive>Delete</DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      </div>
    </li>
  ))}
</ul>

Options & variations

  • Wrap rows in <a> for navigation, or <button> for in-page selection.
  • Drop the chevron when the row is non-navigable.
  • Use truncate on title and subtitle to prevent overflow.
  • Settings dropdown by default. When listings render items in a collection (the typical case), include our standard settings dropdown at the right edge of each row — see Dropdown menu for the trigger pattern (bare Ellipsis, no border).

Data table

A vertical key/value table for displaying record details. Each row is a horizontal stripe with a title on the left and a value on the right. Rows stack on mobile; from md, the title takes 1 column and the value spans 3 (out of 4); from xl, the value spans 4 (out of 5) so wider screens give the value more room while the title stays the same size.

Status
Active
Owner
Marie Chen
Description
A longer value can wrap and flow across multiple lines without breaking the visual rhythm of the table. On wider screens the value takes more room so long copy doesn't have to wrap as aggressively.
Notes
Drop any node in here — text, a Badge, a list, a small grid of nested values.
Updated
May 16, 2026 at 1:27pm

When to use

  • Summarizing the fields of a single record (a video, a profile, a settings panel).
  • Read-only metadata views where each line is a labeled value.
  • Anywhere a long stack of <h3> + paragraph blocks is harder to scan than a labeled grid.

When not to use

  • Collections of similar items — use Listings instead.
  • Dense, multi-column tabular data — use a real <table>.
  • Editable forms — use the Forms primitives.

Sample code

<DataTable>
  <DataRow title="Status">
    <Badge tone="accent">Active</Badge>
  </DataRow>
  <DataRow title="Owner">Marie Chen</DataRow>
  <DataRow title="Description">
    A longer value can wrap and flow across multiple lines without breaking the
    visual rhythm of the table.
  </DataRow>
  <DataRow title="Updated">May 16, 2026 at 1:27pm</DataRow>
</DataTable>

Options & variations

  • <DataRow title> accepts any node — usually a short label, but can include an icon or a small badge.
  • Values are text-ink-body by default. Drop in any node: text, a Badge, a list, a small grid of nested values.
  • Responsive grid: stacked (single column) under md — title sits above value, divider rhythm preserved. From md (768px), 4 columns with title md:col-span-1 and value md:col-span-3. From xl (1280px), 5 columns with value xl:col-span-4.
  • Wrap the table in a card (rounded-lg border border-hairline) if it needs to feel like its own panel.

Callout

A simple bordered container for emphasizing a chunk of content inside a longer page. Apply .callout to any element to get a hairline border, rounded corners, and comfortable padding.

Heads up — this is a callout. Wrap any content that needs a visual container (warning, tip, summary, side note) in .callout.

When to use

  • Tips, notes, warnings, or summary boxes inside long-form content.
  • Pull-out info that should feel separate from surrounding paragraphs.
  • Empty states or zero-data prompts in a panel.

When not to use

  • Modal or dialog content — use the Dialog primitive.
  • Items in a list — use the Listings pattern.
  • Form field grouping — use a fieldset, not a callout.

Sample code

<div className="callout">
  <p>
    Heads up — this is a callout. Wrap any content that needs a
    visual container (warning, tip, summary, side note) in
    <code>.callout</code>.
  </p>
</div>

Options & variations

  • .callout is intentionally minimal: hairline border, rounded corners, padding. No background or color tone — pair with bg-surface or bg-accent-faded if you want emphasis.
  • Compose with text utilities (text-ink-display, text-ink-muted) and inline icons for richer variants (warning, info, success).

Heading scale

All six heading levels at their base sizes. Headings inherit the display font, semibold weight, and tightened line-height from base styles — no utility classes required.

The quick brown fox

The quick brown fox

The quick brown fox

The quick brown fox

The quick brown fox
The quick brown fox

When to use

  • Use semantic heading levels for document outline.
  • Skip-level only when visual hierarchy demands it.

When not to use

  • Don't override font-family or color on headings.
  • Don't use a heading element purely for styling.

Sample code

<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>

H1

Page-level heading. One per page.

Page title

Sample code

<h1>Page title</h1>

H2

Major section heading inside a page.

Section heading

Sample code

<h2>Section heading</h2>

H3

Sub-section heading.

Sub-section

Sample code

<h3>Sub-section</h3>

H4

Minor heading inside a sub-section.

Minor heading

Sample code

<h4>Minor heading</h4>

H5

Card / inline heading.

Card title

Sample code

<h5>Card title</h5>

H6

Eyebrow / label-style heading. Uppercase, tracked, muted color.

Eyebrow label

Sample code

<h6>Eyebrow label</h6>

Anchor (a)

Inline links use the accent color with no underline. Hover darkens the accent slightly.

When to use

  • Real navigation in prose.
  • External references in body copy.

When not to use

  • For action triggers — use a Button (variant="link").
  • Inside dense UI nav lists — those have their own styles.

Sample code

<a href="/path">Read more</a>

Paragraph (p)

Body text element. Inherits color, font, and line-height from base styles.

A paragraph carries the bulk of textual content. Its color is ink-body, its line-height is 1.6, and it does not need any utility classes for its baseline appearance.

Sample code

<p>A paragraph of body text.</p>

Strong

Emphasized inline text. Bumps weight to 600 and color to ink-display.

Most of this sentence is body weight, but this part is emphasized.

Sample code

<p>This is <strong>important</strong>.</p>

Lists (ul / ol)

Lists are unstyled by default — no bullets, no numbers, no padding — so they're safe to use as semantic containers in nav, sidebars, and listings. To get default disc/decimal styling back, wrap the list (or its parent) in .body-content.

ul (default)
  • Unstyled first item
  • Unstyled second item
  • Unstyled third item
ul inside .body-content
  • Bulleted first item
  • Bulleted second item
  • Bulleted third item
ol (default)
  1. Unstyled first item
  2. Unstyled second item
  3. Unstyled third item
ol inside .body-content
  1. Numbered first item
  2. Numbered second item
  3. Numbered third item

Sample code

{/* unstyled — for nav, sidebars, listings */}
<ul>
  <li>One</li>
  <li>Two</li>
</ul>

{/* default disc/decimal — for prose */}
<div className="body-content">
  <ul>
    <li>One</li>
    <li>Two</li>
  </ul>
</div>

List item (li)

Individual list item. Unstyled by default; gets disc/decimal markers and indentation only when its parent list is inside .body-content.

  • A single list item inside .body-content.

Sample code

<div className="body-content">
  <ul>
    <li>A single list item.</li>
  </ul>
</div>

Blockquote

Pull quote with an accent left border and italic, muted color. Use for attribution-style quotes inside body content.

"Make it work, make it right, make it fast — and only after that, make it pretty."

Sample code

<blockquote>"A pithy quote here."</blockquote>

Label / legend

Form-field labels (<label>) and <legend> use the design system's standard label style by default — small text, medium weight, ink-display color. No utility classes needed. The base rule applies to all <label> elements; for the special case of a label used as a wrapper around a checkbox or radio, add font-normal text-ink-body to override.

Sample code

<label htmlFor="email">Email address</label>
<Input id="email" type="email" placeholder="you@example.com" />

Horizontal rule

Visual break between content blocks. Renders as a 1px hairline line with vertical margin.

Above the rule.


Below the rule.

Sample code

<hr />