Skip to content

Layout primitives

Nine primitives compose almost every page, plus a handful of one-line helpers. They are deliberately unopinionated about what they contain, and they are meant to be put on a component rather than around it:

<div class="ads-card__foot ads-cluster">…</div>

Why layout.css loads last

Both selectors above are one class, so the cascade is decided by file order. With layout first, the card's own display: flex would win and the cluster would silently do nothing. A layout primitive that loses to the thing it is applied to is not a primitive.

The primitives

ads-stack
Vertical rhythm. Children are spaced by one gap; no per-element margins to collapse or double. The default container for a form, a page, a card body.
ads-cluster
A horizontal group that wraps. Buttons in a toolbar, filters above a table, tags in a row.
ads-spread
A row with the first child pushed left and the last pushed right. A card head with a title and an action.
ads-grid
Auto-fitting columns. The track is minmax(min(var(--ads-col, 16rem), 100%), 1fr) — the min(…, 100%) is what keeps it from overflowing a 320px screen. Set --ads-col to change the target column width, or use ads-col-sm / ads-col-lg.
ads-with-aside
Content plus a sidebar that drops below when there is no room, with no media query — the main pane has flex-grow: 999 so it takes everything until the aside's basis no longer fits.
ads-split

A list beside what the selected row shows. A mail client, a master-detail view, every "pick one of these, edit it here" screen. Like ads-with-aside it stacks with no media query: --ads-split is the list's width, --ads-detail is the floor the second pane will not go below, and the pair wraps when both no longer fit. That is the whole point — a split lives inside a card inside a shell, so it never has the viewport's width, and a split nested in another split has half of what its parent has.

<div class="ads-split" style="--ads-split: 30rem">
  <div class="ads-split__head">…</div>   <!-- optional: stays put while the page scrolls -->
  …
</div>
ads-split__head
A pane's own header — a search box, a toolbar — that stays in view while the page scrolls under it.
ads-viewport
A fixed-height pane that scrolls its own overflow, for a list too long to put in the document. Unlike ads-table-scroll it scrolls vertically. Its geometry is render-time data — see Data and lists.
ads-board
Fixed-width columns scrolling sideways as a unit: a kanban, a pipeline, a roadmap. The columns are a fixed width on purpose, because an auto-fitting board reflows while you are dragging a card across it. --ads-board-col sets the width.
ads-centered
A full-height grid that centres one panel — a sign-in page, an error page. Put the panel in ads-centered__inner, which caps it at reading width.

Getting one item out of line

ads-fill
Take the remaining room in a flex row, and allow truncation (min-width: 0).
ads-push
margin-inline-start: auto — shove this item and everything after it to the end.
ads-full
width: 100%.

Modifiers

<div class="ads-stack ads-gap-sm">…</div>   <!-- tighter -->
<div class="ads-stack ads-gap-lg">…</div>   <!-- roomier -->
<div class="ads-grid ads-col-sm">…</div>    <!-- narrower columns -->
<div class="ads-grid ads-col-lg">…</div>    <!-- wider columns -->

Gaps come from the space scale, so they follow --ads-space-scale like everything else.

The gap classes set gap, which does not inherit, so they change the element that carries them and nothing below it. They used to set --ads-gap, which does inherit — an ads-gap-sm on a page wrapper quietly became the gap of every ads-cluster, ads-grid and ads-split inside it. If you want a nested layout tighter, put the class on the nested layout.

Utilities

ads-muted
Secondary text colour.
ads-truncate
One line, ellipsis. Carries its own min-width: 0, which is what lets it shrink inside a flex row rather than overflow it — so it works on its own, and pairs with ads-fill only when you also want the item to take the spare room.
ads-center / ads-end
text-align, nothing more. For centring a block, use ads-centered or a margin: auto from ads-container.
ads-toned
Take the colour of the current tone (see below).
ads-skip
The skip-to-content link. Put it first in <body>; it is visually hidden until focused.

Tones instead of variants

Components that can carry an intent read --ads-tone rather than growing a modifier per intent:

<span class="ads-status ads-tone-warning">Degraded</span>
<div class="ads-alert ads-tone-danger">…</div>
<div class="ads-progress ads-tone-success">…</div>

Six tones — brand, success, warning, danger, info, neutral — each setting five variables the component reads: --ads-tone (readable ink), --ads-tone-solid (the fill), --ads-tone-tint, --ads-tone-fill and --ads-tone-line. Adding a seventh intent is one line in derive.css, not one line per component.

Not everything is a card

Border, fill, radius and shadow each say separate object. Spending all four on every block flattens the hierarchy until nothing stands out. Lead with a card when the content really is a discrete object; otherwise a heading and an ads-stack are enough.

Reflow

Everything here is reflow-safe at 320 CSS pixels, which is a 1280px window at 400% zoom — the setting someone with low vision actually uses. If you add a layout of your own, the rule that catches most mistakes is: no absolute length may act as a floor. min-width: 22rem and minmax(22rem, 1fr) both overflow a 20rem viewport; min(22rem, 100%) does not.