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.
Available Tokens
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.
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.
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.
Borders
Structural hairlines in three weights.
Current state
The shared current/selected fill (P10) and the shift color mixed into hovers.
Brand
Primary and accent families with their soft, subtle, border, text, and on-* companions.
Feedback
Danger, success, and warning -- for state, never for resting chrome (P4).
Interaction
Links, the focus ring, selection, and the range thumb.
Shadows
Type scale
--font-size-2xsSphinx of black quartz, judge my vow.
--font-size-xsSphinx of black quartz, judge my vow.
--font-size-baseSphinx of black quartz, judge my vow.
--font-size-mdSphinx of black quartz, judge my vow.
--font-size-lgSphinx of black quartz, judge my vow.
--font-size-xlSphinx of black quartz, judge my vow.
--font-size-2xlSphinx of black quartz, judge my vow.
--font-size-3xlSphinx of black quartz, judge my vow.
Chrome text sizes
--ui-font-size-xsSphinx of black quartz, judge my vow.
--ui-font-size-smSphinx of black quartz, judge my vow.
--ui-font-size-baseSphinx of black quartz, judge my vow.
--ui-font-size-lgSphinx 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-3xlChrome space
--ui-space-4xs--ui-space-3xs--ui-space-2xs--ui-space-xs--ui-space-sm--ui-space-md--ui-space-lgRadius
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.
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.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. 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.