Overview

What to expect

Brio is a CSS library built around native web-platform features. This page is the mental model -- how to think when you're using brio, what each piece is for, and what common patterns look like. It is not a rule reference (see PRINCIPLES.md in the repo for that), and it is not an API catalog (see the per-component pages for that). It's the page to read once, early, so the rest of the docs click.

A note on the trade: brio isn't a drop-in library. It works when you commit to its mental model -- compositions instead of bespoke layout, tokens instead of selector overrides, ARIA instead of state classes. There's less to memorize than you might expect (a few principles, a handful of composition primitives, a token grammar), and one good way to express most patterns; there's more to understand up front. The investment is one-time. After that, things tend to do what you'd guess.

Influences worth naming up front: Every Layout by Heydon Pickering and Andy Bell -- the source of the idea that layout is best expressed as a handful of named composition primitives like .stack and .cluster; and CUBE CSS, also by Andy Bell, whose Composition / Utility / Block / Exception structure is brio's organizing skeleton. brio diverges in places (the exception layer uses class prefixes instead of data-*, and the slot-class rule is stricter -- see PRINCIPLES.md P2 and P7), but the core instincts are theirs.

Principles behind brio

Compose; don't invent

Layout comes from small, named composition primitives -- like .stack and .grid -- one per recurring layout problem. Components handle theming, naming, and state. A component does not reinvent layout internally.

What this looks like in practice: an alert is a themed veneer paired with the .media composition, not a self-contained widget with its own figure / body slot classes.

Heads up

The alert carries theming (is-warning remaps tokens). Layout comes from .media. Two composable pieces, one element.

<div class="alert media is-warning">
    <svg class="media-figure" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3z"></path>
        <path d="M12 9v4"></path>
        <path d="M12 17h.01"></path>
    </svg>
    <div class="media-body">
        <strong>Heads up</strong>
        <p>The alert carries theming (is-warning remaps tokens). Layout comes from .media. Two composable pieces, one element.</p>
    </div>
</div>

The tradeoff: the root element collects multiple class names. The tree in return has fewer named slot classes and reads closer to semantic HTML. Code review shifts from "which of six alert slots is this?" to "this is media; I already know media."

State binds to attributes, not classes

A navigation link for the current page uses aria-current="page", not .is-active. A disclosed panel uses [open]. A pressed toggle uses [aria-pressed="true"]. A loading button uses [aria-busy="true"]. CSS reads the attribute:

<nav class="cluster" aria-label="Example nav">
    <a class="nav-link" href="#">Home</a>
    <a class="nav-link" href="#" aria-current="page">Products</a>
    <a class="nav-link" href="#">Pricing</a>
</nav>

Benefits that follow from this single rule:

  • Server-rendered markup is complete. No <a class="is-active"> that only works after a JS hydration step -- the first byte of HTML already reflects the state correctly.
  • Screen readers get the state for free. "current page" is an announced role; .is-active is a CSS class with no meaning to assistive tech.
  • :has() composes naturally. .card:has([aria-current]) { border-color: primary } without inventing a .card.has-active-item class.

Classes in brio do name things: blocks (.card, .alert), variants and placement (is-*, as-*), exceptions (no-*). They do not own interactive state.

Native first

When the platform has grown a capability, brio uses it:

  • Overlays use native <dialog> and the Popover API. Focus trapping, Escape, the backdrop -- all native behavior. Brio's custom elements sit on top to add animated exit and backdrop-click dismissal; they don't reimplement what the browser already does.
  • Invoker buttons use native command and commandfor attributes (a CSSWG-tracked feature now shipping across browsers, with a small polyfill for laggards).
  • Disclosure widgets are <details>. Brio adds the chevron rotation and height animation; the open/close toggling is the browser's job.
  • Colors are oklch(). Layout is logical properties. Selectors use :has(), :where(), @container, @layer. Every one of these has been a framework feature in the past; all are native now.

What this means for you: brio's JS surface stays small because it doesn't duplicate the browser. When something like a dialog feels surprisingly light, that's because it mostly is the browser, with brio adding a wrapping element for the things the browser doesn't offer (an animated close, a backdrop-click handler).

Accessibility is baseline

WCAG 2.1 AA is the floor for every brio component. Contrast is checked at the token level; focus visibility is built into base styles; keyboard interaction is part of every interactive component. These aren't opt-ins.

When you style with brio, you're starting from a surface that is already keyboard-navigable, already announces its state to screen readers, already meets AA contrast in both light and dark modes. Your job when building on top is not to break it:

  • If you add interactive behavior, bind state to ARIA or native attributes. If you find yourself reaching for a .is-active class, first check whether an ARIA attribute fits -- it usually does.
  • If you override colors with your own tokens, verify the contrast holds. The tokens page ships a live contrast checker you can compare against.
  • If you create custom components with focus states, use :focus-visible with a visible outline -- don't suppress focus rings.

Restraint by default

Reach for the subtler end of every scale first. Shadow sm before md. Tonal shift before chroma. 1px before 2px. The question to keep in mind: what is the smallest change that communicates this signal?

This shows up in brio's defaults. Atoms stay flat. Surfaces elevate only when tinted differentiation cannot carry the lift on its own. State changes are color shifts, not depth changes. Borders are 1px unless emphasis genuinely earns 2px. Animation durations are short and easing curves are gentle.

For CSS authors building on brio: when you override a token toward the louder end of a scale (a bigger shadow, a 2px border, a higher-chroma background), pause to check whether the design needs the lift or whether a quieter alternative (more whitespace, clearer hierarchy, a different composition) would carry the signal. The defaults pull the system back toward quiet; the intentional moments of loudness should be deliberate.

Dynamic content where it's needed

Most brio components are static-renderable -- the HTML you author at page load is what the user sees. A small set of components (toast and similar) need to receive content at runtime: a save confirmation, a server-pushed status, an htmx swap. brio handles those via a single pattern: pre-author the container with its live-region landmark; let any path insert items into it; brio's MutationObserver wires the lifecycle.

The same toast can be triggered three ways without changing the library: a button calling brio.toast.show(), an htmx or datastar response swapping a finished toast into the container, or a framework component calling container.appendChild(). brio doesn't care which path delivered the markup -- the contract is "put a .toast in the container; brio takes it from there." This keeps server-driven and client-driven code paths reading the same way.

How the pieces fit

Brio is structured in layers. You'll touch the higher layers day to day; the lower layers are mostly there to be correct by default.

Tokens

Two-tier design tokens. Palette stops (--gray-200, --blue-500) live privately inside the token file; semantic tokens (--color-primary, --color-surface, --space-md) are what components consume. Override semantic tokens at a scope to re-skin without fighting specificity.

For spacing there's a third tier between raw tokens and component vars: a density grammar (--ui-pad-y-snug, --gap-relaxed) that pairs padding and gap into consistent steps. Components use the grammar; you override the grammar when you want everything in a subtree to feel tighter or airier.

See the Tokens page for the full catalog and a live contrast checker.

Composition primitives

Small, named layout primitives. Each one does one thing well: .stack for vertical rhythm, .cluster for horizontal wrapping groups, .grid for auto-fill grids, .flank for in-page two-column splits (the Every Layout sidebar pattern), .media for figure + body rows, and several more.

When in doubt, reach for a composition primitive before writing bespoke CSS. If a pattern keeps recurring in your own code and no primitive fits, that's a candidate for promotion to the library -- or for a pattern in your project-specific docs.

See the Composition page for each primitive with intent, an example, and pitfalls.

Components

Named blocks with theming and (sometimes) behavior, like alerts, dialogs, and form inputs. Each component declares an archetype that tells you how it fits with composition primitives:

  • Atom. Single-element primitive with no children needed. Variants via is-* and as-*. Examples: .button, .badge, .spinner.
  • Composition primitive. Layout only, no visual identity. Theming via scoped CSS vars. Examples: .stack, .grid, .media.
  • Slotted container. Structural CSS that composition primitives cannot absorb. Has named slot classes that pass the P2 test. Examples: .card, .dialog.
  • Semantic wrapper. No classes beyond the root; all styling on semantic HTML descendants. Examples: .table, .prose.
  • Themed veneer. Theming class paired with a composition primitive. Adds identity (background, border, color variant); composition handles layout. Examples: .alert with .media, .toast with .media.

Full definitions and the rules each archetype follows are in PRINCIPLES.md P3.

Utilities

Single-purpose classes for cases that don't earn their own component or token override. Kept deliberately narrow; brio is not a utility framework and will not grow into one.

What's deliberately not here: utility-framework-style spacing classes (p-8, m-4, gap-2, and so on). brio's compositions already handle the work those utilities target -- .stack for vertical rhythm, .cluster and .grid for gap, the density grammar (--ui-pad-*) for component padding. By the time you'd reach for a spacing utility, one of those three usually already fits. A parallel utility scale would duplicate work the composition layer already does, and shifts layout decisions from "name the shape" to "stack the modifiers."

If your project genuinely needs additional utilities, add them to your own CSS. brio's brio.css declares the cascade-layer order (reset, tokens, animations, base, composition, blocks, rescopes, utilities, overrides) so consumer styles can drop into the right layer without specificity wars:

  • utilities for new single-purpose helpers alongside brio's (a project-specific aspect ratio, a content-width clamp).
  • overrides for project-wide adjustments to brio's component output (remapping --card-padding-block for every card on a marketing site, retoning --color-primary for a brand).

The style attribute is fair game in moderation, especially for one-off token overrides where authoring a class would be more ceremony than the change deserves -- style="--card-padding-block: var(--space-lg)" on a single hero card, for example. The line to watch is drift: the same inline style appearing on three or more elements is a class waiting to be born; the same property being set inline across a section is usually a single token override on the section root.

See the Utilities page for the catalog.

Anti-patterns

A short list of moves that fight brio's grain. None are illegal; they just make future changes harder than necessary.

  • Reaching for a new component class for a layout need. Ask first: does a composition primitive do this? Most of the time, yes.
  • Reaching for spacing utilities (p-8, m-4, gap-2) for one-off layout adjustments. brio doesn't ship them on purpose. Use a composition primitive, override the component's scoped padding token, or set a single inline --token on the element — see Utilities above for the reasoning.
  • Using .is-active, .is-open, .is-loading, or .is-disabled as the source of truth for interactive state. The corresponding ARIA or native attribute is usually the right hook. Keep is-* for variants, sizes, and transient animation states.
  • Consuming palette stops (--gray-200, --blue-500) directly. They're private to the token file. Consume semantic tokens (--color-border, --color-primary) or a component's scoped vars.
  • Overriding styles with higher specificity selectors instead of scoped tokens. If a component exposes a token, override the token (in a scoped class, in the overrides layer, or as an inline style for a true one-off). Reach for !important only when the cascade has genuinely backed you into a corner (rare, in practice).
  • Reimplementing a native pattern. If you're about to write JS to build a modal from scratch, stop -- <dialog> is already there. Same for disclosure, popover, and invoker buttons.

Where to go next

Read the docs in whichever order fits what you're building. A natural starting order: Tokens (the vocabulary), then Composition (the layout grammar), then individual components as they come up. The component pages assume the vocabulary from the first two.

If you're curious about the technical rules that shape brio's design (when slot classes earn their name, why a component picks one archetype over another), that's in PRINCIPLES.md in the repo. It's written for people extending brio, not using it -- but reading it answers "why is this named this way?" questions.