Tokens

Brio heavily uses CSS properties throughout the library. So, they tend to be the design vocabulary: --color-primary, --space-md, --radius-lg, etc. This makes it efficient to re-skin the existing themes, or create a new theme by overriding tokens at scope. For the purposes of the library, I call them tokens.

Tokens are organized in two tiers. Palette stops (--primary-9, --neutral-3) are the internal building blocks; components don't normally consume them directly. Semantic tokens (--color-primary, --color-border) are the public API, which helps create a stable set of names that carry meaning across themes or projects. A third tier, the density grammar, sits between raw space tokens and component-scoped tokens to make padding and gap consistent across the system. This is somewhat abstract, but it helps to ensure that there are some consistent padding and gap values across themes or projects.

Space and type tokens ship two parallel scales. The clamp()-based tokens like --space-* and --font-size-* tokens are fluid. They change with the viewport, and are used for layout, prose, and display. The UI tokens all start with --ui-*, such as --ui-space-* and --ui-font-size-*. They are used for UI chrome (like buttons or inputs) where consistency across viewports matters more than scaling.

Then, what I call the density grammar can be used to bind consistent padding and gap values throughout the library, which helps keep relative spacing throughout the design. At least, that was the idea. More about that later.

Colors

The color system also splits most of the tokens int a couple of groups. There are chromatic colors, such as --color-primary, -danger, -success, -warning. These are used for state, such as focus, checked, selected, hovered, and invalid. Then, there are neutral colors, such as --color-surface*, --color-border*, --color-text*, --color-fill*. These are used for resting chrome, such as backgrounds, borders, and text.

Surfaces are mostly neutral; everything painted on them is surface-aware. Text, fills, and borders each have a global default plus -on-raised and -on-float companions. The goal was to create consistency so that AA contrast holds on elevated surfaces. Context-bound components (a dropdown inside a popover) also reference the surface variants. Context-free components (button, input, badge) use the global role token.

The fill scale is a neutral tint family, parallel to and orthogonal from elevation. You can use fills for components that should be tinted with a neutral surface, such as an avatar disc, a skeleton placeholder, or a striped table row, rather than to signal that it sits above or below another surface.

The current-state fill (--color-state-fill and companions) is what Brio paints behind a persistent "you are here" indicator. For example, a dropdown's active item or a sidebar's current page. It is a single library-wide knob: the default is neutral, and a project overrides it at :root to flip the mood of every consuming component at once. You can use the small Mood picker at the top of this docs site to preview the effect. It's subtle, but it's there. Look for changes in the color of the active dropdown item or the current page in the sidebar.

Each status family (primary, accent, danger, success, warning) ships a consistent set of tokens: base, subtle background, soft fill, border, text, and on-color. Hover and active states are not tokenized; they're derived in component CSS via color-mix() against the --color-shift anchor (black in light mode, white in dark). You can override the family base and the interaction states should follow automatically. For colors, the soft scale is a quieter set of colors for data display (progress bars, charts), whereas the louder status colors are bolder, and meant to be more noticeable for communication.

The color scale is a 12-step palette, with each step representing a different shade of color. See more details on the Theming page.

You'll find most of the tokens available below. They are generated from the tokens.css file in the source. Swatches adn text samples change with the theme. So, you can select a different theme to see how it affects the tokens.

Palette stops

Twelve steps per family, each with a documented role. Steps 1 through 5 are backgrounds, 6 through 8 borders, 9 the brand color, 11 and 12 text.

--neutral-1 --neutral-2 --neutral-3 --neutral-4 --neutral-5 --neutral-6
--neutral-7 --neutral-8 --neutral-9 --neutral-10 --neutral-11 --neutral-12
--primary-1 --primary-2 --primary-3 --primary-4 --primary-5 --primary-6
--primary-7 --primary-8 --primary-9 --primary-10 --primary-11 --primary-12
--accent-1 --accent-2 --accent-3 --accent-4 --accent-5 --accent-6
--accent-7 --accent-8 --accent-9 --accent-10 --accent-11 --accent-12
--danger-1 --danger-2 --danger-3 --danger-4 --danger-5 --danger-6
--danger-7 --danger-8 --danger-9 --danger-10 --danger-11 --danger-12
--success-1 --success-2 --success-3 --success-4 --success-5 --success-6
--success-7 --success-8 --success-9 --success-10 --success-11 --success-12
--warning-1 --warning-2 --warning-3 --warning-4 --warning-5 --warning-6
--warning-7 --warning-8 --warning-9 --warning-10 --warning-11 --warning-12

Semantic colors

The tier that components use internally. Redefine these in the @overrides layer.

Surfaces

The page and its elevation tiers. Absolute colors; everything else on this list is surface-aware.

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

Text

Text roles, with the -on-raised and -on-float companions so contrast holds on elevated surfaces. A text token is only meaningful next to the surface it reads on, so each row below paints the sample sentence in the token on its paired surface.

--color-textSphinx of black quartz, judge my vow.
--color-text-mutedSphinx of black quartz, judge my vow.
--color-text-subtleSphinx of black quartz, judge my vow.
--color-text-inverseSphinx of black quartz, judge my vow.
--color-text-subtle-on-raisedSphinx of black quartz, judge my vow.
--color-text-muted-on-raisedSphinx of black quartz, judge my vow.
--color-text-subtle-on-floatSphinx of black quartz, judge my vow.
--color-text-muted-on-floatSphinx of black quartz, judge my vow.

Fill

Neutral tint fills for inputs, wells, and hover shifts.

--color-fill-subtle --color-fill --color-fill-strong --color-fill-stronger --color-fill-on-float

Borders

Structural hairlines in three weights.

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

Current state

The shared current/selected fill (P10) and the shift color mixed into hovers.

--color-shift --color-state-fill --color-state-fill-on-raised --color-state-fill-on-float

Brand

Primary and accent families with their soft, subtle, border, text, and on-* companions.

--color-primary --color-primary-subtle --color-primary-soft --color-primary-border --color-primary-text --color-on-primary --color-on-primary-subtle --color-on-primary-soft --color-accent --color-accent-subtle --color-accent-soft --color-accent-border --color-accent-text --color-on-accent --color-on-accent-subtle --color-on-accent-soft

Feedback

Danger, success, and warning -- for state, never for resting chrome (P4).

--color-danger --color-danger-subtle --color-danger-soft --color-danger-border --color-danger-text --color-on-danger --color-on-danger-subtle --color-on-danger-soft --color-success --color-success-subtle --color-success-soft --color-success-border --color-success-text --color-on-success --color-on-success-subtle --color-on-success-soft --color-warning --color-warning-subtle --color-warning-soft --color-warning-border --color-warning-text --color-on-warning --color-on-warning-subtle --color-on-warning-soft

Interaction

Links, the focus ring, selection, and the range thumb.

--color-thumb --color-link --color-link-hover --color-link-visited --color-focus --color-selection-bg --color-selection-text

Shadows

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

Type scale

--font-size-2xs

Sphinx of black quartz, judge my vow.

--font-size-xs

Sphinx of black quartz, judge my vow.

--font-size-base

Sphinx of black quartz, judge my vow.

--font-size-md

Sphinx of black quartz, judge my vow.

--font-size-lg

Sphinx of black quartz, judge my vow.

--font-size-xl

Sphinx of black quartz, judge my vow.

--font-size-2xl

Sphinx of black quartz, judge my vow.

--font-size-3xl

Sphinx of black quartz, judge my vow.

Chrome text sizes

--ui-font-size-xs

Sphinx of black quartz, judge my vow.

--ui-font-size-sm

Sphinx of black quartz, judge my vow.

--ui-font-size-base

Sphinx of black quartz, judge my vow.

--ui-font-size-lg

Sphinx of black quartz, judge my vow.

Space

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

Chrome space

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

Radius

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

Font weights

--font-weight-thinSphinx of black quartz, judge my vow.
--font-weight-extralightSphinx of black quartz, judge my vow.
--font-weight-lightSphinx of black quartz, judge my vow.
--font-weight-normalSphinx of black quartz, judge my vow.
--font-weight-mediumSphinx of black quartz, judge my vow.
--font-weight-semiboldSphinx of black quartz, judge my vow.
--font-weight-boldSphinx of black quartz, judge my vow.
--font-weight-blackSphinx of black quartz, judge my vow.

Leading

--leading-tightSphinx of black quartz, judge my vow.
Sphinx of black quartz, judge my vow.
--leading-headingSphinx of black quartz, judge my vow.
Sphinx of black quartz, judge my vow.
--leading-bodySphinx of black quartz, judge my vow.
Sphinx of black quartz, judge my vow.
--leading-looseSphinx of black quartz, judge my vow.
Sphinx of black quartz, judge my vow.

Contrast

This table shows how the semantic tokens pair with their expected text colors. It also shows how the tokens pair with their expected background colors. These are verified against WCAG 2.1 and APCA. They may not be exact, but they are close. 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 backgrounds 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. So, you can switch dark mode or apply a theme override and re-render to see updated values.

Density grammar

The density grammar is a small abstraction to help manage padding and gutters across the system. While building this, I found that spacing values started to drift at times, so this helps me share a common set of spacing values throughout the library, especially for UI elements. This also helps when I want to change how a theme looks with only a few token changes.

The grammar splits on the relationship each word describes: pad is the space within a box, and gutter is the space between boxes. Chrome uses the fixed forms: --ui-pad-* and --ui-gutter-* resolve to --ui-space-*, so UI padding and UI gutters stay identical at every viewport. Layout uses the fluid form: --gutter-* resolves to --space-*, so spacing between elements in compositions (stack, cluster, grid) changes with the viewport.

Each family maps the six tiers to its own values, so a tier name is a density intent, not a shared number: --ui-pad-block-tight and --gutter-tight are both tight, but one resolves to 8px and the other to 4px. The tier tells you which value within a family; it never promises the same value across families.

Inline-axis padding, --ui-pad-inline-*

The fixed within-space on the inline axis. Component padding tokens resolve here, so button, input, and nav chrome keeps its inline padding identical at every viewport. I keep the inline value one step above the block value at every tier, as that tends to look better in most cases.

Tier Token Resolves to
spacious --ui-pad-inline-spacious ui-space-lg
relaxed --ui-pad-inline-relaxed ui-space-md
snug --ui-pad-inline-snug ui-space-sm
tight --ui-pad-inline-tight ui-space-xs
compact --ui-pad-inline-compact ui-space-2xs
dense --ui-pad-inline-dense ui-space-3xs

Block-axis padding, --ui-pad-block-*

The fixed within-space on the block axis, one step below the inline-axis family at every tier.

Tier Token Resolves to
spacious --ui-pad-block-spacious ui-space-md
relaxed --ui-pad-block-relaxed ui-space-sm
snug --ui-pad-block-snug ui-space-xs
tight --ui-pad-block-tight ui-space-2xs
compact --ui-pad-block-compact ui-space-3xs
dense --ui-pad-block-dense ui-space-4xs

Square padding, --ui-pad-square-*

Icon-shaped atoms, such as the dismiss button and the icon button, take equal padding on both axes, and the two axis families disagree by design. This family gives those atoms one shared value per tier. Only the four dense tiers exist: snug is the largest square padding the icon components use, so spacious and relaxed have no tokens here.

Tier Token Resolves to
snug --ui-pad-square-snug ui-space-xs
tight --ui-pad-square-tight ui-space-2xs
compact --ui-pad-square-compact ui-space-3xs
dense --ui-pad-square-dense ui-space-4xs

Chrome gutter, --ui-gutter-*

The fixed between-space inside components: between a button's icon and its label, between a dropdown row's glyph and its text, between a panel's header and its body. Before this family existed, chrome used the fluid gutters, so a button's interior gap changed with the viewport while its padding did not. The fixed form keeps the interior stable.

Tier Token Resolves to
spacious --ui-gutter-spacious ui-space-md
relaxed --ui-gutter-relaxed ui-space-sm
snug --ui-gutter-snug ui-space-xs
tight --ui-gutter-tight ui-space-3xs
compact --ui-gutter-compact ui-space-4xs
dense --ui-gutter-dense ui-space-4xs

Layout gutter, --gutter-*

The fluid between-space for layout: the stack, cluster, grid, reel, and switcher gutters, plus the between-block rhythm margins in flow. These resolve to the fluid --space-* scale, so layout spacing changes with the viewport.

Tier Token Resolves to
spacious --gutter-spacious space-md
relaxed --gutter-relaxed space-sm
snug --gutter-snug space-xs
tight --gutter-tight space-3xs
compact --gutter-compact space-4xs
dense --gutter-dense space-4xs

When a component needs spacing outside the grammar, such as a card that should have more space on mobile or a section divider that must match a specific visual rhythm, you can use the raw --space-* tokens directly, or the fixed --ui-space-* scale when the exception is chrome that must stay stable.

Radius

The radius tokens provide a set of consistent border-radius values. Each scales according to a global --radius-scale multiplier: lower values for tighter, subtler rounding; higher for softer, more rounded borders.

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. For example, a badge with variable text, a toggle track, or a range slider. The rounded-ends look should hold with pill. 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 shape ever fails. An ellipse usually signals "wrong shape" louder than an overstretched pill.

Shadows

There are 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.

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. These were named so that I didn't have to invent arbitrary numbers, and they are easier to remember and more descriptive.

Token Used by
--zindex-raised Mild elevation (a focused member of a joined input group)
--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. I know there are workarounds, but this was just as simple. The canonical reference is this table. If you need to use different breakpoints, override the small number of components that need them.

Name Width Used by
sm 40rem .hide-below-sm / .hide-above-sm
md 48rem .hide-below-md / .hide-above-md; brio-color-mode .is-cycle-below-md group/cycle swap
lg 64rem .hide-below-lg / .hide-above-lg; sidebar hide / sidebar-toggle show

Roles and their palette steps

Every semantic role points at one palette step, so you can override the role to re-theme semantics, or override the stop to re-palette everything that reads it. The steps follow the 12-step role table on the Theming page. The pattern across the status families:

Role Palette step Used for
--color-text, --color-text-muted neutral-12, neutral-11 Body and secondary text
--color-surface, -sunken, -raised neutral-1, neutral-2 Page and elevated surfaces
--color-surface-float, --color-fill neutral-3 Overlays and neutral tints
--color-fill-strong, --color-fill-stronger neutral-5, neutral-7 Stronger neutral tints
--color-border-subtle to --color-border-stronger neutral-4 to neutral-7 Borders, subtle to strong
--color-X, --color-X-text X-11 Solid brand fills; family text on the page
--color-X-subtle X-3 Tinted backgrounds (badges, alerts, toasts)
--color-X-soft X-5 Tinted fills (progress, hovers)
--color-X-border X-7 Tinted borders
--color-on-X white, or X-2 in dark mode Text on solid brand fills
--color-on-X-subtle, --color-on-X-soft X-12 Guaranteed text on tints

Warning is the exception: amber's brand stop is step 9, because yellow's identity sits at high lightness. So --color-warning points at warning-9, and --color-on-warning is a fixed dark olive in both modes.

Text on a tinted background follows one recipe: --color-on-X-subtle on --color-X-subtle. The on-colors are the high-contrast end of each ramp, so the pairing clears AA by construction. Badges, alerts, toasts, pressed buttons, and mark all use it. --color-X-text is for family-colored text on the page surface, like .text-success or inline code. Keep the two apart: step 11 text on a step 3 tint only reaches about 4.2:1, which is why the contrast table above checks the on-subtle pairing.

Overriding tokens

Brio declares its tokens with :where(html) -- zero specificity -- so an override of any strength wins: a bare :root, a type selector, a class, an inline style. The tokens are the defaults, which means you can override them at the tier you need.

Override them where needed. Most tokens are scoped at the :root level, but some are scoped at within components. So, override them at the tier you need. You can use the @overrides layer to stay within the Brio layering system.

/* Semantic tokens reach a whole subtree. Component tokens
   reach the instance they are set on. */
.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>/* Semantic tokens reach a whole subtree. Component tokens
   reach the instance they are set on. */
.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-*) are declared and consumed on the component itself: set them on the instance. Semantic tokens (--color-primary, --radius-scale) are declared on the root but consumed at each component: set them on any container and the entire subtree reskins.

Palette stops (--primary-11) are the one tier that does not scope. The semantic tier resolves var(--primary-N) at the root, so a stop overridden on a container changes nothing inside it -- the semantic token already carries its resolved value past the container. To re-theme a subtree at the palette level, paste the stops and the semantic tokens that reference them into the scope; the indirection then re-resolves at the container:

/* Palette-level theming of one subtree: both tiers, pasted
   into the scope, so the indirection re-resolves here. */
.bs-section {
    --primary-11: light-dark(oklch(57.8% 0.228 260), oklch(81% 0.103 260));
    --color-primary: var(--primary-11);
    --color-primary-text: var(--primary-11);
    --color-link: var(--primary-11);
    /* ...the rest of the semantic tier that reads the primary family */
}
<pre class="code"><code>/* Palette-level theming of one subtree: both tiers, pasted
   into the scope, so the indirection re-resolves here. */
.bs-section {
    --primary-11: light-dark(oklch(57.8% 0.228 260), oklch(81% 0.103 260));
    --color-primary: var(--primary-11);
    --color-primary-text: var(--primary-11);
    --color-link: var(--primary-11);
    /* ...the rest of the semantic tier that reads the primary family */
}</code></pre>

Color mode follows the same rule: .light-theme / .dark-theme on a container set color-scheme for that subtree, so light-dark() values resolve per scope -- a light section on a dark page, no separate tokens required.

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