Layout primitives
Every screen is built from 3 layout primitives —
.bo-stack (vertical rhythm), .bo-cluster
(a row that wraps), .bo-grid (cards that reflow) —
inside the app shell. Each takes custom-property knobs you set
inline or in your own CSS.
Choosing one
| Use | When | Knobs |
|---|---|---|
| .bo-stack | Vertical rhythm between blocks — form fields, sections, card contents | --bo-stack-gap |
| .bo-cluster | A horizontal row that wraps — toolbars, button groups, tag lists | --bo-cluster-gap |
| .bo-grid | Equal-width cards that reflow by available width | --bo-grid-min, --bo-grid-gap |
| .bo-app-shell | The page skeleton — header, sidebar, main | container-driven |
Stack
View code
<div class="bo-stack" style="max-inline-size: 20rem">
<button class="bo-btn bo-btn--secondary">Default gap</button>
<button class="bo-btn bo-btn--secondary">— space-4 between blocks</button>
</div>
<div class="bo-stack bo-stack--tight" style="max-inline-size: 20rem; margin-block-start: 1.5rem">
<button class="bo-btn bo-btn--secondary">--tight</button>
<button class="bo-btn bo-btn--secondary">— space-2, for dense field groups</button>
</div>
<div class="bo-stack bo-stack--loose" style="max-inline-size: 20rem; margin-block-start: 1.5rem">
<button class="bo-btn bo-btn--secondary">--loose</button>
<button class="bo-btn bo-btn--secondary">— space-6, for page sections</button>
</div> One gap, three sizes.
--tight and --loose work by setting --bo-stack-gap — so an inline knob on a
--tight element wins over the modifier.
Cluster
View code
<!-- default: everything packs to the start and wraps -->
<div class="bo-cluster">
<button class="bo-btn">Save</button>
<button class="bo-btn bo-btn--secondary">Cancel</button>
<span class="bo-badge bo-badge--warning">Draft</span>
</div>
<!-- --split: first item left, rest right — the toolbar shape -->
<div class="bo-cluster bo-cluster--split" style="margin-block-start: 1.5rem">
<strong>Purchase orders</strong>
<span class="bo-cluster">
<button class="bo-btn bo-btn--secondary">Export</button>
<button class="bo-btn">New order</button>
</span>
</div>
<!-- --end: everything right — dialog footers -->
<div class="bo-cluster bo-cluster--end" style="margin-block-start: 1.5rem">
<button class="bo-btn bo-btn--secondary">Cancel</button>
<button class="bo-btn">Approve</button>
</div> Items wrap instead of overflowing, so a
toolbar degrades gracefully on narrow screens.
--split pushes the last child to the far edge (nest a
plain cluster to keep several buttons together);
--end right-aligns everything.
Grid
View code
<!-- --bo-grid-min sets the smallest a column may get before
the grid drops to fewer columns. Resize the window to see it reflow. -->
<div class="bo-grid" style="--bo-grid-min: 10rem">
<div class="bo-widget"><div class="bo-widget__body">Card 1</div></div>
<div class="bo-widget"><div class="bo-widget__body">Card 2</div></div>
<div class="bo-widget"><div class="bo-widget__body">Card 3</div></div>
<div class="bo-widget"><div class="bo-widget__body">Card 4</div></div>
</div> Two behaviours worth knowing.
Columns are auto-fill, not
auto-fit: a lone card keeps its column width
instead of stretching across the row — deliberate, so a dashboard
doesn't reflow into one enormous card when a filter empties it (use
your own auto-fit grid if you want stretching).
And the column floor is min(--bo-grid-min, 100%), which
is what stops a wide minimum from overflowing a narrow container —
so you can set --bo-grid-min aggressively and trust it.
App shell
<div class="bo-app-shell"> <!-- container: bo-shell -->
<header class="bo-navbar bo-app-shell__header">…</header>
<nav class="bo-sidebar-nav bo-app-shell__sidebar">…</nav>
<main class="bo-app-shell__main">…</main>
</div> The shell is a named container
(bo-shell), so the sidebar collapses to icon-only once the
shell is narrow — not the viewport, which is why it behaves
correctly inside a split view. Labels stay in the accessibility tree when
collapsed. The band itself is stated once, from source, in the
shell contract; details
on Container queries.
Composing them
View code
<!-- The whole recipe: shell holds the page, stack gives it
vertical rhythm, a split cluster is the toolbar, a grid holds cards.
In a real shell this wrapper is the page's <main> (see the shell
markup below); it is a div here so this preview does not add a
second main landmark to the docs page. -->
<div class="bo-app-shell__main">
<div class="bo-stack">
<div class="bo-cluster bo-cluster--split">
<strong style="font-size: var(--bo-font-size-lg)">Cost centre CC-4021</strong>
<span class="bo-cluster">
<button class="bo-btn bo-btn--secondary">Export</button>
<button class="bo-btn">New order</button>
</span>
</div>
<div class="bo-grid" style="--bo-grid-min: 12rem">
<div class="bo-widget"><div class="bo-widget__body">
<dl class="bo-kv bo-kv--rows"><div><dt>Open</dt><dd class="bo-u-tabular">14</dd></div></dl>
</div></div>
<div class="bo-widget"><div class="bo-widget__body">
<dl class="bo-kv bo-kv--rows"><div><dt>Approved</dt><dd class="bo-u-tabular">62</dd></div></dl>
</div></div>
<div class="bo-widget"><div class="bo-widget__body">
<dl class="bo-kv bo-kv--rows"><div><dt>Spend</dt><dd class="bo-u-tabular">$184,200</dd></div></dl>
</div></div>
</div>
</div>
</div> This is the whole system: the shell holds the page, a stack gives it rhythm, a split cluster is the toolbar, a grid holds the cards. Every ERP screen in Patterns is this shape with more content.
Knobs generated from the CSS
| Knob | Default | Applies to |
|---|---|---|
| --bo-stack-gap | var(--bo-space-4) | .bo-stack |
| --bo-cluster-gap | var(--bo-space-2) | .bo-cluster |
| --bo-grid-min | 16rem), 100% | .bo-grid |
| --bo-grid-gap | var(--bo-space-4) | .bo-grid |
Set them inline for a one-off
(style="--bo-grid-min: 12rem") or in your own CSS for a
whole region. Screen-reader-only text is
.bo-visually-hidden, documented with the other
utilities.