Tokens

Every brio component consumes design tokens rather than raw values. Tokens are the vocabulary: --color-primary, --space-md, --radius-lg, etc. Consumers re-skin brio by overriding tokens at a scope, not by authoring new rules.

Tokens are organized in two tiers. Palette stops (--primary-500, --neutral-200) are the internal building blocks; components don't consume them directly. Semantic tokens (--color-primary, --color-border) are the public API -- stable names that carry meaning across themes. A third tier, density grammar, sits between raw space tokens and component-scoped tokens to make padding and gap consistent across the system.

Space and type each ship two parallel scales: --space-* and --step-* are fluid (clamp()-based, breathing with viewport) for layout, prose, and display; --ui-space-* and --ui-text-* are fixed for UI chrome (like buttons or inputs) where consistency across viewports matters more than scaling. The density grammar binds padding to the fixed side and gap to the fluid side -- padding is chrome, gap is layout.

Colors

Semantic -- text

Text tokens for running copy, muted labels, subtle hints, and inverse contexts. The muted and subtle tokens also have per-surface variants for AA contrast on elevated surfaces -- see Surface-context variants.

--color-text -- The quick brown fox jumps over the lazy dog.

--color-text-muted -- The quick brown fox jumps over the lazy dog.

--color-text-subtle -- The quick brown fox jumps over the lazy dog.

--color-text -- The quick brown fox jumps over the lazy dog.

--color-text-muted -- The quick brown fox jumps over the lazy dog.

--color-text-subtle -- The quick brown fox jumps over the lazy dog.

(--color-text-inverse is shown alongside --color-surface-inverse in the surfaces section.)

Semantic -- surface

Four elevation levels plus an inverse surface. Light mode conveys elevation through borders and shadows; dark mode uses progressive lightness (see PRINCIPLES.md P10 in the repo for the rationale).

--color-surface-sunken
--color-surface
--color-surface-raised
--color-surface-float
--color-surface-inverse
--color-surface-sunken
--color-surface
--color-surface-raised
--color-surface-float
--color-surface-inverse

Semantic -- fill

Neutral tint scale, parallel to (and orthogonal from) elevation. Use when a component wants a tinted neutral surface for its own sake -- avatar disc, skeleton placeholder, hover-active fill, striped row -- rather than to signal it sits above or below another surface. Light mode steps progressively darker than the page surface; dark mode steps progressively lighter. The medium tier may share a computed value with --color-surface-sunken; the names describe intent. Per-surface variants are available for components on elevated surfaces -- see Surface-context variants.

--color-fill-subtle
--color-fill
--color-fill-strong
--color-fill-subtle
--color-fill
--color-fill-strong

Semantic -- current-state fill (mood)

The fill brio paints behind a persistent "you are here" indicator -- like a dropdown's active item or a sidebar's current link. The token is a single library-wide knob that lets a project pick a visual mood without touching any component CSS. The brio default is neutral (--color-fill); consumer projects override at :root to flip the mood across every consuming component at once. See PRINCIPLES.md P10 for the full rationale.

The docs site is showing chromatic mode. The override in src/docs/docs.css sets the family to --color-primary-subtle so the documentation reads as the chromatic dogfood. Click the mood cycle button in the page header (the small filled circle next to the color-mode toggle) to walk through all three presets -- the swatches below and every active row on this page move together.

--color-state-bg
--color-state-bg-on-raised
--color-state-bg-on-float
--color-state-bg
--color-state-bg-on-raised
--color-state-bg-on-float

The three preset values consumer projects pick from:

  • transparent -- minimal mood; the active row's text + bold weight carries the indicator without a fill.
  • var(--color-fill) family -- neutral mood; subtle gray band. brio's default.
  • var(--color-primary-subtle) -- chromatic mood; primary-tinted band. The docs site's choice.

The fill is purely the chromatic-intensity knob. The structural indicator -- primary text color and semibold weight -- stays put across every mood, which is what makes the transparent preset safe to ship: components remain unambiguously indicated even with no background at all. Per-component scoped tokens (--dropdown-item-bg-current, --pagination-item-active-bg, etc.) read from this family by default, so narrower overrides at any scope still compose through the cascade.

Semantic -- borders

Subtle, default, and strong weights. Per-surface variants are available for elevated surfaces -- see Surface-context variants.

--color-border-subtle
--color-border
--color-border-strong
--color-border-subtle
--color-border
--color-border-strong

Semantic -- status families

Each status family (primary, accent, danger, success, warning) ships a consistent set of tokens: base, subtle background, soft fill, border, text, and on-color. Components use them by name: --color-primary for a primary button background, --color-on-primary for readable text on top.

Hover and active states are not tokenized -- they're derived from the base color in component CSS via light-dark(color-mix(... var(--color-shift) X%), color-mix(... var(--color-shift) Y%)). The --color-shift token (defined in tokens.css) is the system-wide mix anchor: black in light mode, white in dark mode. Mixing toward it darkens in light mode and lightens in dark mode. Per-mode percentages compensate for lightness headroom asymmetry between modes. Override --color-{family} at scope and the interaction states follow automatically; override --color-shift to retune the character of every derived state across the system. See src/css/button.css for the derivation pattern.

The soft variant is a quieter parallel scale for data-display contexts (like progress bars or charts) where the loud status colors fight each other. Rule of thumb: state-communication chrome (like alerts and validation) stays loud; data display goes soft. Soft fills have lower contrast than their loud counterparts -- prefer labels outside the fill. When labels-inside is required, use --color-on-{family}-soft (the family's -900 stop, dark in both modes) to clear AA contrast.

Primary

--color-primary
--color-primary-subtle
--color-primary-soft
--color-primary-border

--color-primary-text -- The quick brown fox jumps over the lazy dog.

--color-primary
--color-primary-subtle
--color-primary-soft
--color-primary-border

--color-primary-text -- The quick brown fox jumps over the lazy dog.

Accent

--color-accent
--color-accent-subtle
--color-accent-soft
--color-accent-border

--color-accent-text -- The quick brown fox jumps over the lazy dog.

--color-accent
--color-accent-subtle
--color-accent-soft
--color-accent-border

--color-accent-text -- The quick brown fox jumps over the lazy dog.

Danger

--color-danger
--color-danger-subtle
--color-danger-soft
--color-danger-border

--color-danger-text -- The quick brown fox jumps over the lazy dog.

--color-danger
--color-danger-subtle
--color-danger-soft
--color-danger-border

--color-danger-text -- The quick brown fox jumps over the lazy dog.

Success

--color-success
--color-success-subtle
--color-success-soft
--color-success-border

--color-success-text -- The quick brown fox jumps over the lazy dog.

--color-success
--color-success-subtle
--color-success-soft
--color-success-border

--color-success-text -- The quick brown fox jumps over the lazy dog.

Warning

--color-warning
--color-warning-subtle
--color-warning-soft
--color-warning-border

--color-warning-text -- The quick brown fox jumps over the lazy dog.

--color-warning
--color-warning-subtle
--color-warning-soft
--color-warning-border

--color-warning-text -- The quick brown fox jumps over the lazy dog.

--color-link -- The quick brown fox jumps over the lazy dog.

--color-link-hover -- The quick brown fox jumps over the lazy dog.

--color-link-visited -- The quick brown fox jumps over the lazy dog.

--color-focus
--color-selection-bg

--color-link -- The quick brown fox jumps over the lazy dog.

--color-link-hover -- The quick brown fox jumps over the lazy dog.

--color-link-visited -- The quick brown fox jumps over the lazy dog.

--color-focus
--color-selection-bg

Surface-context variants

Two surface-context patterns ship as tokens:

  • --color-text-{muted,subtle}-on-{raised,float} -- four tokens for muted and subtle text on elevated surfaces (e.g., text inside a card for raised; inside a dialog for float). Tuned per surface so they clear AA contrast across the elevation ramp.
  • --color-fill-on-float -- the tinted hover bg used by float-surface components (like a dropdown's item hover) and the rescopes layer. Derived from --color-surface-float via --color-shift so it tracks any surface or shift override automatically.

Other surface-context fill and border variants (-on-raised, -subtle-on-*, -strong-on-*, all --color-border-*-on-*) existed for symmetry but had no consumers; dropped. State shifts (hover, active) are derived in component CSS via the --color-shift pattern; see the status-families intro above.

Two rules

  1. Surfaces are absolute. A --color-surface-* token resolves to one global value per mode regardless of nesting. A card paints var(--color-surface-raised) and that's the end of its elevation logic.
  2. Reach for the variant when the deployment surface is known. Context-bound components reference the surface-tuned token directly:
.card { color: var(--color-text-muted-on-raised); }
.dropdown-item { --dropdown-item-bg-hover: var(--color-fill-on-float); }

Context-free components (like .button or .input) use the role tokens (var(--color-text-muted), var(--color-fill)) and accept the page-surface value.

Cross-component composition

When a context-free atom is nested inside an elevated surface in a known pattern -- a ghost button placed inside a dialog, for example -- its global token resolution would collide with the surface bg in dark mode. The rescopes cascade layer (between blocks and utilities in brio.css) hosts explicit selector-based overrides for these patterns; consumers can extend it with their own composition rules.

Palette stops

The building blocks. 12 steps per family, role-mapped (Radix-aligned). Palette stops are private to the token file -- components consume semantic tokens above, not these. Shown here for reference and for overriding specific stops when tuning a theme.

Each step has a documented role rather than an arithmetic position:

StepRole
1App background
2Subtle background
3UI element background (default)
4UI element background (hover)
5UI element background (active)
6Subtle borders, separators
7UI element borders, focus rings
8Hovered UI element borders
9Solid background -- the brand stop, highest chroma. Same value in both modes.
10Hovered solid background
11Low-contrast text (APCA Lc 60 against step 2)
12High-contrast text (APCA Lc 90 against step 2)

Source values are adapted from Radix Colors v3.x, converted from sRGB to OKLCH; the specific scales brio uses for each family are recorded in tokens.css. To build a custom palette, follow the same 12-step methodology -- anchor step 9 at your saturated brand point and walk outward, tuning each step against APCA contrast targets for its role.

Neutral (1 -- 12)

1
2
3
4
5
6
7
8
9
10
11
12

Primary (1 -- 12)

1
2
3
4
5
6
7
8
9
10
11
12

Accent (1 -- 12)

1
2
3
4
5
6
7
8
9
10
11
12

Danger (1 -- 12)

1
2
3
4
5
6
7
8
9
10
11
12

Success (1 -- 12)

1
2
3
4
5
6
7
8
9
10
11
12

Warning (1 -- 12)

1
2
3
4
5
6
7
8
9
10
11
12

All six families share the same 12-step structure with role-mapped values. Warning (yellow) is asymmetric: its step 9 sits at L=85% in both modes (yellow's chromatic identity is at high lightness), so the loud band runs lighter than other families and dark text is needed through step 10.

Contrast

Every semantic token pairing used by brio components is verified against two contrast models. WCAG 2.1 is the established standard (AA = 4.5:1 normal text, 3:1 large/bold). APCA is the perceptually-tuned model proposed for WCAG 3 -- it accounts for polarity (light-on-dark vs dark-on-light) and uses perceptual luminance instead of raw sRGB ratios. APCA Lc 75+ is body-text-equivalent; Lc 60+ for 18px+; Lc 45+ for large/bold only.

Both are shown because they often disagree on borderline cases. WCAG can rate dark-on-saturated as passing while APCA flags it as weak (a known WCAG 2 weakness with mid-luminance saturated colors); conversely, some Radix-derived loud bgs pass APCA for body text but only clear WCAG AA-large. brio targets WCAG AA-normal where possible; the APCA column is informational and a reference for WCAG 3 readiness.

The table below computes ratios live from whatever the active theme is -- switch dark mode or apply a theme override and re-render to see updated values.

Type scale

Two parallel scales. --step-* is fluid, for prose and display content that should breathe with viewport. --ui-text-* is fixed, for UI chrome where every component should read the same size at every screen width.

Fluid -- --step-*

Fluid type scaling between 360px and 1240px viewports. Each step is a clamp() that grows with the viewport up to a ceiling. Steps go from --step--2 (small captions) through --step-5 (display headings). Used by .prose, h1-h6, alert and card body, and any context where the content is user-authored text.

--step--2 The quick brown fox

--step--1 The quick brown fox

--step-0 The quick brown fox (body baseline)

--step-1 The quick brown fox

--step-2 The quick brown fox

--step-3 The quick brown fox

--step-4 The quick brown fox

--step-5 The quick brown fox

Fixed -- --ui-text-*

Fixed type sizes for component chrome. Buttons, inputs, nav links, badges, table cells, and similar controls read the same pixel size at every viewport. Aligned with shadcn/radix conventions.

--ui-text-xs The quick brown fox

--ui-text-sm The quick brown fox

--ui-text-base The quick brown fox

--ui-text-lg The quick brown fox

Rule of thumb: if the element holds user-authored prose, use --step-*. If it's UI chrome -- a label, control, meta text, table cell -- use --ui-text-*.

Line height

  • --leading-tight -- tight labels, sizing buttons
  • --leading-heading -- headings (default for h1-h6 in base.css)
  • --leading-body -- body copy (default on <body>)
  • --leading-loose -- large-text emphasis contexts

Font weights

The full numeric scale is exposed: --font-weight-thin, -extralight, -light, -normal, -medium, -semibold, -bold, -black. Most brio components use medium or semibold for emphasis; reserve the extremes for display typography.

Space scale

Two parallel scales. --space-* is fluid, for layout, composition primitives, and bespoke spacing that should breathe. --ui-space-* is fixed, consumed by the density grammar to keep component chrome stable across viewports.

Fluid -- --space-*

Fluid space tokens, clamp()-based between 360px and 1240px viewports. Used directly for block/inline sizes, margins, section spacing, stack/cluster/grid gaps, and one-off spacing that should adapt. The density grammar's --gap-* aliases resolve to these.

--space-4xs
--space-3xs
--space-2xs
--space-xs
--space-sm
--space-md
--space-lg
--space-xl
--space-2xl
--space-3xl

In addition to single stops, the scale ships "one-up pairs" that interpolate between adjacent stops (--space-xs-sm, --space-sm-md, etc.) for cases where you want something between two sizes as the viewport grows.

Fixed -- --ui-space-*

Fixed atomic sizes for component chrome. Consumed by the density grammar's --ui-pad-* aliases so button, input, nav, badge, and table padding stay identical across viewports.

--ui-space-4xs
--ui-space-3xs
--ui-space-2xs
--ui-space-xs
--ui-space-sm
--ui-space-md
--ui-space-lg

Rule of thumb: if the spacing is inside a component (padding, icon-to-text gap on a button), use the density grammar, which resolves to --ui-space-* for padding. If it's between components or inside a layout (stack, cluster, grid, section spacing), use --space-* directly or via the grammar's --gap-* aliases.

Density grammar

Six tiers sitting between raw space tokens and component vars. Components consume these for padding and gap so density stays consistent across the system. Horizontal padding is one step larger than vertical at each tier -- the classic "inline > block" ratio that makes components read as refined.

The grammar splits by purpose: --ui-pad-* resolves to --ui-space-* (fixed) so chrome padding stays identical at every viewport, while --gap-* resolves to --space-* (fluid) so spacing between elements -- whether chrome gap (button icon-to-text) or layout gap (stack, cluster, grid) -- breathes with viewport.

Tier Horizontal padding (fixed) Vertical padding (fixed) Gap (fluid)
spacious--ui-pad-x-spacious (ui-space-lg)--ui-pad-y-spacious (ui-space-md)--gap-spacious (space-md)
relaxed--ui-pad-x-relaxed (ui-space-md)--ui-pad-y-relaxed (ui-space-sm)--gap-relaxed (space-sm)
snug--ui-pad-x-snug (ui-space-sm)--ui-pad-y-snug (ui-space-xs)--gap-snug (space-xs)
tight--ui-pad-x-tight (ui-space-xs)--ui-pad-y-tight (ui-space-2xs)--gap-tight (space-3xs)
compact--ui-pad-x-compact (ui-space-2xs)--ui-pad-y-compact (ui-space-3xs)--gap-compact (space-4xs)
dense--ui-pad-x-dense (ui-space-3xs)--ui-pad-y-dense (ui-space-4xs)--gap-dense (space-4xs)

A separate --ui-pad-square-* family (snug, tight, compact, dense) covers icon-shaped atoms (dismiss button, icon button) where both axes should be equal.

When a component needs spacing outside the grammar -- a card that should breathe on mobile, a section divider that must match a specific visual rhythm -- reach for raw --space-* directly.

Radius

Four stops plus full (pill) and circle. A global --radius-scale multiplier lets you tune the entire system: lower values for tighter/subtle rounding, higher for softer/approachable chrome.

--radius-sm
--radius-md
--radius-lg
--radius-xl
--radius-pill
--radius-circle

Pill vs circle

--radius-pill and --radius-circle look identical on a square box -- both produce a circle. They diverge on rectangular boxes: --radius-pill (9999px) keeps the ends fully rounded as the box stretches, producing a pill; --radius-circle (50%) scales with the box dimensions, producing an ellipse.

--radius-pill on rectangle
--radius-circle on rectangle

Reach for --radius-pill when the box can grow either way -- a badge with variable text, a toggle track, a range slider -- and the rounded-ends look should hold. Reach for --radius-circle when the shape must be a true circle on a square box (dismiss button, avatar) and you want the radius to break visibly if the square enforcement ever fails. An ellipse signals "wrong shape" louder than an over-stretched pill.

Shadows

Four elevation shadows, built from two shadow-color tokens (--color-shadow and --color-shadow-lg). In dark mode the shadow tokens grow heavier but become visually subtle; elevation in dark mode is conveyed by surface lightness, not shadow.

--shadow-sm
--shadow-md
--shadow-lg
--shadow-xl

Fonts

  • --font-base -- system sans stack (Apple system, Segoe, Cantarell, etc.). Used for body and headings by default.
  • --font-heading -- defaults to --font-base. Override to differentiate headings from body.
  • --font-mono -- Menlo, Consolas, Monaco stack. Used by .code and by docs chrome.

Sample in --font-base. The quick brown fox jumps over the lazy dog.

Sample in --font-mono. The quick brown fox jumps over the lazy dog.

Motion

Transitions and animations use two separate families:

  • Transitions (--transition-fast, -normal, -slow) -- shorthands including an easing. Use for hover / focus micro-interactions on color, background, border, opacity.
  • Animations (--duration-* + --ease-*) -- durations and easing curves separately, for multi-property entry/exit animations (dialog open, drawer slide, details expand). Easing options: --ease-out (entrances), --ease-in (exits), --ease-in-out (symmetric, two-way), --ease-spring (overshoot for playful UI).

Both families are zeroed under prefers-reduced-motion: reduce. If you author your own animations with these tokens, you get reduced-motion support for free.

Z-index scale

Hardcoded layer values for overlapping UI. Named so you don't have to invent numbers.

TokenUsed by
--zindex-raisedMild elevation (sticky cell within a table, for example)
--zindex-stickySticky headers, sticky sidebars
--zindex-dropdownDropdowns, popovers (non-modal)
--zindex-drawerSide drawers
--zindex-dialogModal dialogs
--zindex-toastToast notifications
--zindex-tooltipTooltips (above dialogs)
--zindex-importantEscape hatch; don't reach for it

Breakpoints

Breakpoints are hardcoded in media queries because CSS custom properties can't be used inside @media conditions. They live in the code, not as tokens; this table is the canonical reference.

NameWidthUsed by
sm40rem.hide-below-sm / .hide-above-sm
md48rem.hide-below-md / .hide-above-md
lg64rem.hide-below-lg / .hide-above-lg; sidebar hide / sidebar-toggle show

Overriding tokens

Tokens are overridable at any scope. Set them on :root to theme globally; set them on a component or container to affect only that subtree.

/* Theme an entire section */
.docs-chrome {
    --color-primary: oklch(55% 0.15 130);  /* green-leaning */
    --radius-scale: 0.5;                    /* tighter rounding */
}

/* Tune a single component instance */
.my-drawer {
    --drawer-width: 22rem;
}
<pre class="code"><code>/* Theme an entire section */
.docs-chrome {
    --color-primary: oklch(55% 0.15 130);  /* green-leaning */
    --radius-scale: 0.5;                    /* tighter rounding */
}

/* Tune a single component instance */
.my-drawer {
    --drawer-width: 22rem;
}</code></pre>

Component-scoped tokens (--drawer-width, --card-bg, etc.) are listed on each component's reference page.