Composition
The layout grammar
Brio components don't reinvent layout. They theme, name, and manage state; layout comes from a small set of named composition primitives. This page catalogs each primitive with intent, a working example, and the overridable tokens. The primitives are layout-only -- no colors, borders, or backgrounds -- so they compose with themed components freely.
Each primitive is small enough to internalize. A brio site's custom CSS shrinks because layout lives in these classes, reused everywhere.
Full token defaults for every primitive live in
src/css/composition.css; each primitive's section
below names its tokens and design intent, not the values.
Flow
Vertical rhythm via margins on adjacent siblings. Does not set a flex context -- elements keep their natural formatting. When you want vertical stacking with flex features (alignment, child flex properties), use Stack instead.
<div class="flow">
<div class="docs-box">First</div>
<div class="docs-box">Second</div>
<div class="docs-box">Third</div>
</div>Tokens: --flow-space
Quirks worth knowing:
Implementation: .flow > * + *
applies margin-block-start to every direct child
except the first. The first child has no top margin.
Per-child override: set --flow-space
on an individual child to tweak the space above that
child. This is the smallest, most useful escape hatch -- it doesn't
affect any other element:
<div class="flow" style="--flow-space: var(--space-sm);">
<div class="docs-box">Standard gap above</div>
<div class="docs-box" style="--flow-space: var(--space-xl);">Extra gap above this one</div>
<div class="docs-box">Standard gap again</div>
</div>
Nesting quirk (and the fix): when a nested
.flow is a direct child of an outer .flow,
the nested flow's own --flow-space overrides the gap
above itself. The rule reads the property from the element
it's targeting, not from the parent. This is usually not what you
want -- you set the inner --flow-space to tune spacing
between the nested flow's children, but it leaks into the outer
flow's rhythm too.
Without a wrapper, the gap above the nested flow collapses to the nested flow's tight spacing. The dashed box shows where the inner flow starts -- notice how close it sits to item A:
<div class="flow" style="--flow-space: var(--space-md);">
<div class="docs-box">A (outer flow)</div>
<div class="flow" style="--flow-space: var(--space-3xs); border: 1px dashed var(--color-border); padding: var(--space-sm); border-radius: var(--radius-sm);">
<small style="color: var(--color-text-subtle);">This inner flow's margin-block-start reads its own --flow-space (tight), not the outer's (medium).</small>
<div class="docs-box">B (nested, tight gap)</div>
<div class="docs-box">C (nested, tight gap)</div>
</div>
<div class="docs-box">D (outer flow)</div>
</div>
The fix is to wrap the nested flow in any non-flow element -- a
bare <div> is fine. The wrapper inherits the
outer flow's --flow-space so its own top margin is
correct, while the nested flow's --flow-space stays
scoped to its own children. Same dashed box, same content, same
inner spacing -- only the wrapper changes, and now the gap above
the inner flow matches the outer rhythm:
<div class="flow" style="--flow-space: var(--space-md);">
<div class="docs-box">A (outer flow)</div>
<div>
<div class="flow" style="--flow-space: var(--space-3xs); border: 1px dashed var(--color-border); padding: var(--space-sm); border-radius: var(--radius-sm);">
<small style="color: var(--color-text-subtle);">Wrapper div inherits the outer --flow-space for its own top margin; inner --flow-space stays contained to this flow's children.</small>
<div class="docs-box">B (nested, tight gap)</div>
<div class="docs-box">C (nested, tight gap)</div>
</div>
</div>
<div class="docs-box">D (outer flow)</div>
</div>
The wrapper is invisible in layout (no padding, no display change)
-- it exists only to isolate the custom-property scope. If you see
a bare <div> around a .flow in brio
code, this is usually why.
No flex context: align-items /
justify-content don't do anything on .flow
-- use .stack or plain
display: flex if you need them.
CSS Reference: Flow
Vertical rhythm via margin on adjacent siblings
Source: src/css/composition.css
Tokens
| Token | Default | Description |
|---|---|---|
--flow-space | var(--gap-spacious | Vertical spacing between flow children |
Classes
| Class | Description |
|---|---|
.flow | component root |
Stack
Vertical layout with a flex context. Use when you need flex
features: align-items, justify-content,
children using flex: 1 to grow, etc. For pure vertical
rhythm without flex, prefer Flow.
<div class="stack is-snug">
<div class="docs-box">One</div>
<div class="docs-box">Two</div>
<div class="docs-box">Three</div>
</div>Tokens: --stack-gap
Modifiers:
.is-snug,.is-tight-- tighter gap tiers.is-centered-- cross-axis centering (align-items: center); compose with.text-centerwhen item text should center too
.is-centered is the missing piece for centered-column
patterns -- empty states, marketing intros, simple hero blocks
where an icon, heading, and action sit in a centered column.
Pair it with .center for the
width clamp and .text-center for the text alignment.
Nothing here yet
When something arrives, it'll show up in this space.
<div class="center stack is-centered text-center" style="--center-max: 22rem">
<span class="text-subtle">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</span>
<h3>Nothing here yet</h3>
<p class="text-muted">When something arrives, it'll show up in this space.</p>
</div>CSS Reference: Stack
Flex-based vertical layout
Source: src/css/composition.css
Tokens
| Token | Default | Description |
|---|---|---|
--stack-gap | var(--gap-snug) | Gap between stack children |
Classes
| Class | Description |
|---|---|
.stack | component root |
.is-snug | Tighter gap between stack children |
.is-tight | Tightest gap between stack children |
.is-centered | Cross-axis centering for stack items |
Cluster
Horizontal wrapping group with gap. The workhorse for things like toolbars and button rows. Wraps to multiple rows as needed; items never overflow.
<div class="cluster">
<div class="docs-box">One</div>
<div class="docs-box">Two</div>
<div class="docs-box">Three</div>
<div class="docs-box">Four</div>
</div>Tokens:
--cluster-gap--cluster-wrap-- set tonowrapfor single-row clusters--cluster-align--align-itemsvalue
Modifiers:
.is-snug,.is-tight-- tighter gap tiers.is-between--justify-content: space-between.is-center--justify-content: center.is-end--justify-content: flex-end
Growing behavior: .is-between
includes flex: 1 so the cluster grows to fill a
flex-row parent's remaining space (the canonical navbar
bookend pattern). In block or grid parents the grow is inert.
In a flex-column parent -- for example, a child of
.sidebar or .stack -- the cluster
grows vertically, almost never what you want. Wrap the cluster
in a bare block element to neutralize the grow.
Cluster -- navbar-like
With three children, .is-between gives you logo |
centered nav | actions. With two, it's a left/right split. With
four or more, the middle items cluster in the center.
<docs-resize>
<div class="cluster is-between" style="border: 1px dashed var(--color-border); padding: var(--space-sm);">
<strong>Logo</strong>
<div class="cluster"><a class="nav-link" href="#">Home</a><a class="nav-link" href="#">Products</a><a class="nav-link" href="#">Pricing</a></div>
<div class="cluster is-end"><button class="button is-sm">Sign in</button></div>
</div>
</docs-resize>CSS Reference: Cluster
Horizontal wrapping group
Source: src/css/composition.css
Tokens
| Token | Default | Description |
|---|---|---|
--cluster-wrap | wrap | Flex wrap behavior for the cluster |
--cluster-align | center | Cross-axis alignment for cluster items |
--cluster-gap | var(--gap-snug) | Gap between cluster items |
Classes
| Class | Description |
|---|---|
.cluster | component root |
.is-snug | Tighter gap between cluster items |
.is-tight | Tightest gap between cluster items |
.is-between | Space-between distribution with flex grow |
.is-around | Space-evenly distribution with flex grow |
.is-center | Center-justified cluster items |
.is-end | End-justified cluster items |
Grid
Auto-fill responsive grid. Columns size themselves to a minimum width; when the container grows, more columns fit. When it shrinks, columns wrap to new rows. Perfect for card grids, swatch grids, anything that should flow into the available width.
<docs-resize>
<div class="grid" style="--grid-min: 8rem;">
<div class="docs-box">A</div>
<div class="docs-box">B</div>
<div class="docs-box">C</div>
<div class="docs-box">D</div>
<div class="docs-box">E</div>
</div>
</docs-resize>Tokens:
--grid-min-- the minimum column width--grid-gap
Quirk worth knowing:
When the container is narrower than --grid-min, you get a
single full-width column -- not horizontal scroll, not items
overflowing. The minmax(--grid-min, 1fr) formula means
"at least this wide, but grow to fill." At widths that can't fit even
one column at --grid-min, the 1fr side takes over and
items fill the row individually. This is usually what you want;
mention it because "my grid isn't showing multiple columns" often
comes down to a narrow container.
Brio's grid uses auto-fill, which leaves empty column
tracks when you have fewer items than columns. If you want items
to stretch to fill all available width, write a project-specific
auto-fit variant instead.
CSS Reference: Grid
Auto-fill responsive grid
Source: src/css/composition.css
Tokens
| Token | Default | Description |
|---|---|---|
--grid-min | 16rem), 1fr | Minimum column width for auto-fill columns |
--grid-gap | var(--gap-spacious | Gap between grid cells |
Classes
| Class | Description |
|---|---|
.grid | component root |
Flank
In-page two-column split where a fixed-width
secondary column flanks a flexible main column. Wraps to a
stacked single column when the viewport is too narrow for both
side by side. (Every Layout's "Sidebar" pattern, renamed to
avoid overlap with brio's .sidebar component.)
Resize the container below to see the secondary column wrap
under main when main can't hold its --flank-content-min.
The demo overrides --flank-width to 12rem so the
transition happens within the resizable container's range; the
default width would need a wider demo area. Overriding scoped
tokens like this is how you tune any composition primitive
without touching its CSS.
<docs-resize>
<div class="flank" style="--flank-width: 12rem;">
<aside class="docs-flank-aside">Secondary</aside>
<div class="docs-flank-main">Main content</div>
</div>
</docs-resize>Tokens:
--flank-width-- secondary column target width--flank-gap--flank-content-min-- main column wraps under secondary when its width would drop below this
Modifier: .is-reversed swaps source order: main content first in markup, secondary second. Visually, main sits on the inline-start and secondary on the inline-end. Useful when screen-reader order or SEO should lead with content.
<docs-resize>
<div class="flank is-reversed" style="--flank-width: 12rem;">
<div class="docs-flank-main">Main content</div>
<aside class="docs-flank-aside">Secondary</aside>
</div>
</docs-resize>Don't use for full-viewport app shells. Flex-based flanking layouts can't cleanly give a secondary column the full viewport height independent of main's content length. For dashboards, docs sites, and other app shells, use grid -- see App-shell patterns below.
CSS Reference: Flank
Two-column flanking layout: fixed-width secondary plus flexible main
Source: src/css/composition.css
Tokens
| Token | Default | Description |
|---|---|---|
--flank-gap | var(--space-lg | Gap between the secondary and main columns |
--flank-width | 20rem | Flex basis for the secondary column |
--flank-content-min | 50% | Minimum width before wrapping to single column |
Classes
| Class | Description |
|---|---|
.flank | component root |
.is-reversed | Main content first, secondary second |
Switcher
Row layout that switches to a column when the container gets narrow enough that items can't fit side by side. Container-relative, not viewport-relative -- the threshold is based on the container's width, so nested switchers each decide independently.
Resize the container below to see the row-to-column switch at the default threshold:
<docs-resize>
<div class="switcher">
<div class="docs-box">One</div>
<div class="docs-box">Two</div>
<div class="docs-box">Three</div>
</div>
</docs-resize>Tokens:
--switcher-threshold-- width below which items stack--switcher-gap--switcher-align--switcher-wrap
CSS Reference: Switcher
Row-to-column layout based on container width
Source: src/css/composition.css
Tokens
| Token | Default | Description |
|---|---|---|
--switcher-wrap | wrap | Flex wrap behavior for the switcher |
--switcher-align | center | Cross-axis alignment for switcher items |
--switcher-gap | var(--gap-spacious | Gap between switcher items |
--switcher-threshold | 30rem) - 100%) * 999 | Container width at which items switch to column |
Classes
| Class | Description |
|---|---|
.switcher | component root |
Cover
Vertically centered content with a minimum block-size. A first and
last child stay at the top and bottom; anything in between centers
vertically. Default min-height is 100vh -- full
viewport. Useful for landing hero sections, empty states, and
error pages.
<div class="cover" style="--cover-min-height: 12rem; background: var(--color-surface-sunken); border-radius: var(--radius-md);">
<header><strong>Header</strong></header>
<div class="docs-box cover-center">Centered</div>
<footer><small>Footer</small></footer>
</div>Tokens:
--cover-min-height--cover-padding
The .cover-center marker: when you want a specific child to be the "centered" one explicitly, add .cover-center to it. Otherwise, the middle child is inferred.
Quirk worth knowing:
The default --cover-min-height fills the viewport.
That's right for landing heroes and empty-state pages that should
fill the screen. It's wrong for an in-page section, where the
cover would stretch far beyond the surrounding content. If you're
using .cover inside an article, set
--cover-min-height to an explicit size
(e.g. 20rem) up front. There's no way to "let it size
to the content" -- the point of cover is to have a minimum.
CSS Reference: Cover
Vertically centered content with minimum height
Source: src/css/composition.css
Tokens
| Token | Default | Description |
|---|---|---|
--cover-min-height | 100vh | Minimum height for the cover container |
--cover-padding | var(--space-md | Inner padding for the cover container |
Classes
| Class | Description |
|---|---|
.cover | component root |
Frame
Aspect-ratio container. Images, video, and anything else that should maintain a fixed ratio regardless of content. Default is 16:9. Children are centered by default.
<div class="frame" style="--frame-ratio: 4 / 3; background: var(--color-surface-sunken); border-radius: var(--radius-md);">
<div class="docs-box">4 : 3 frame</div>
</div>Tokens:
--frame-ratio--frame-align--frame-justify
CSS Reference: Frame
Aspect ratio container
Source: src/css/composition.css
Tokens
| Token | Default | Description |
|---|---|---|
--frame-ratio | 16 / 9 | Aspect ratio for the frame |
--frame-align | center | Vertical alignment of frame content |
--frame-justify | center | Horizontal alignment of frame content |
Classes
| Class | Description |
|---|---|
.frame | component root |
Reel
Horizontal scroll strip. Items sit in a row that scrolls horizontally rather than wrapping. Useful for things like image galleries or tag strips that shouldn't wrap.
<div class="reel">
<div class="docs-box" style="min-inline-size: 10rem;">Item 1</div>
<div class="docs-box" style="min-inline-size: 10rem;">Item 2</div>
<div class="docs-box" style="min-inline-size: 10rem;">Item 3</div>
<div class="docs-box" style="min-inline-size: 10rem;">Item 4</div>
<div class="docs-box" style="min-inline-size: 10rem;">Item 5</div>
<div class="docs-box" style="min-inline-size: 10rem;">Item 6</div>
</div>Tokens:
--reel-gap--reel-item-size--reel-item-shrink--reel-scrollbar
CSS Reference: Reel
Horizontal scroll strip
Source: src/css/composition.css
Tokens
| Token | Default | Description |
|---|---|---|
--reel-gap | var(--gap-spacious | Gap between reel items |
--reel-scrollbar | thin | Scrollbar display style for the reel |
--reel-item-shrink | 0 | Whether reel items can shrink below their basis |
--reel-item-size | auto | Flex basis for reel items |
Classes
| Class | Description |
|---|---|
.reel | component root |
Center
Constrain maximum width and center horizontally. The standard "readable column" treatment. Adds horizontal gutters so content doesn't hug the edges.
This column is max 30rem wide, centered horizontally, with horizontal gutters.
<div class="center" style="--center-max: 30rem; background: var(--color-surface-sunken); border-radius: var(--radius-md); padding-block: var(--space-md);">
<p>This column is max 30rem wide, centered horizontally, with horizontal gutters.</p>
</div>Tokens:
--center-max-- maximum inline size--center-gutter-- horizontal padding
CSS Reference: Center
Constrained width with auto margins
Source: src/css/composition.css
Tokens
| Token | Default | Description |
|---|---|---|
--center-max | 65rem | Maximum content width |
--center-gutter | var(--ui-pad-x-relaxed | Inline padding that narrows the content area |
Classes
| Class | Description |
|---|---|
.center | component root |
Box
A padded container, optionally with a border and background. Use as a lightweight surface when a full component (card, alert) is more than you need.
Bordered box with default padding.
<div class="box is-bordered">
<p>Bordered box with default padding.</p>
</div>Tokens:
--box-padding--box-bg,--box-border,--box-radius-- active when.is-bordered
Modifier: .is-bordered adds a surface background, border, and radius.
CSS Reference: Box
General-purpose padding container
Source: src/css/composition.css
Tokens
| Token | Default | Description |
|---|---|---|
--box-padding | var(--space-md | Inner padding for the box |
Classes
| Class | Description |
|---|---|
.box | component root |
.is-bordered | Adds background, border, and radius to the box |
Media
The "leading figure + growing body" shape: icon + text, avatar +
content, thumbnail + caption. This is the archetypal figure + text
row -- components like alerts or toasts compose with
.media rather than reinventing their own figure/body
slot classes.
The figure never shrinks; the body grows and wraps. Alignment is controlled by --media-align.
<div class="media">
<svg class="media-figure" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<path d="M12 6v6l4 2"></path>
</svg>
<div class="media-body">
<strong>Figure and body</strong>
<p>The figure never shrinks; the body grows and wraps. Alignment is controlled by --media-align.</p>
</div>
</div>Tokens:
--media-gap--media-align--align-itemsvalue
Slot classes:
.media-figure-- fixed-size leading element (icon, image).flex-shrink: 0..media-body-- growing text element.flex: 1,min-inline-size: 0(see quirk below).
Quirk worth knowing:
.media-body sets min-inline-size: 0
deliberately. Without it, a flex child's minimum size defaults to
its content's intrinsic size, which means long unbreakable
strings (URLs, monospace code, long words) force the media row wider
than its container instead of wrapping. Overriding to
min-inline-size: 0 lets the body shrink below its
content's natural width and wrap. Debugging "why is this overflowing"
in a .media row often comes back to a child that has
lost this property through an override.
A second tweak: headings inside .media-body get
line-height: 1 so the heading's glyph-top aligns with
the figure's top. Normal heading line-height (1.2) would leave the
heading visually sitting lower than the figure. If you see a
heading-plus-icon row where the icon looks "too high," the fix is
probably already there -- check specificity of an override.
CSS Reference: Media
Figure plus text row layout
Source: src/css/composition.css
Tokens
| Token | Default | Description |
|---|---|---|
--media-align | flex-start | Cross-axis alignment for media children |
--media-gap | var(--gap-relaxed | Gap between figure and text |
Classes
| Class | Description |
|---|---|
.media | component root |
CSS Reference: Media Figure
Icon or image slot in a media layout
Source: src/css/composition.css
Tokens
| Token | Default | Description |
|---|---|---|
--media-figure-start | 0.1rem | Block-start margin to nudge the figure vertically |
Classes
| Class | Description |
|---|---|
.media-figure | component root |
Anchor
Positioning context for a small overlay child -- notification dot on a button, status badge on an avatar, unread count. The wrapped element retains its normal layout; the overlay child positions absolutely relative to it.
<span class="anchor">
<button class="button is-sm">Inbox</button>
<span class="anchor-overlay badge is-danger">3</span>
</span>Tokens:
--anchor-offset-- overlay inset from top-end corner
Slot class: .anchor-overlay -- absolutely positioned child; default top-end corner.
CSS Reference: Anchor
Positioning context for an overlay child
Source: src/css/composition.css
Tokens
| Token | Default | Description |
|---|---|---|
--anchor-offset | -0.75em | Position offset for the overlay child |
Classes
| Class | Description |
|---|---|
.anchor | component root |