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).
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.
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.
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.
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-text -- The quick brown fox jumps over the lazy dog.
--color-primary-text -- The quick brown fox jumps over the lazy dog.
Accent
--color-accent-text -- The quick brown fox jumps over the lazy dog.
--color-accent-text -- The quick brown fox jumps over the lazy dog.
Danger
--color-danger-text -- The quick brown fox jumps over the lazy dog.
--color-danger-text -- The quick brown fox jumps over the lazy dog.
Success
--color-success-text -- The quick brown fox jumps over the lazy dog.
--color-success-text -- The quick brown fox jumps over the lazy dog.
Warning
--color-warning-text -- The quick brown fox jumps over the lazy dog.
--color-warning-text -- The quick brown fox jumps over the lazy dog.
Semantic -- link, focus, selection
--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-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.
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-floatvia--color-shiftso 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
- Surfaces are absolute. A
--color-surface-*token resolves to one global value per mode regardless of nesting. A card paintsvar(--color-surface-raised)and that's the end of its elevation logic. - 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:
| Step | Role |
|---|---|
| 1 | App background |
| 2 | Subtle background |
| 3 | UI element background (default) |
| 4 | UI element background (hover) |
| 5 | UI element background (active) |
| 6 | Subtle borders, separators |
| 7 | UI element borders, focus rings |
| 8 | Hovered UI element borders |
| 9 | Solid background -- the brand stop, highest chroma. Same value in both modes. |
| 10 | Hovered solid background |
| 11 | Low-contrast text (APCA Lc 60 against step 2) |
| 12 | High-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)
Primary (1 -- 12)
Accent (1 -- 12)
Danger (1 -- 12)
Success (1 -- 12)
Warning (1 -- 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-xlFonts
--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.codeand 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.
| Token | Used by |
|---|---|
--zindex-raised | Mild elevation (sticky cell within a table, for example) |
--zindex-sticky | Sticky headers, sticky sidebars |
--zindex-dropdown | Dropdowns, popovers (non-modal) |
--zindex-drawer | Side drawers |
--zindex-dialog | Modal dialogs |
--zindex-toast | Toast notifications |
--zindex-tooltip | Tooltips (above dialogs) |
--zindex-important | Escape 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.
| Name | Width | Used by |
|---|---|---|
| sm | 40rem | .hide-below-sm / .hide-above-sm |
| md | 48rem | .hide-below-md / .hide-above-md |
| lg | 64rem | .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.