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:
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)— themin(…, 100%)is what keeps it from overflowing a 320px screen. Set--ads-colto change the target column width, or useads-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: 999so 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-asideit stacks with no media query:--ads-splitis the list's width,--ads-detailis 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. 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-scrollit 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-colsets 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-pushmargin-inline-start: auto— shove this item and everything after it to the end.ads-fullwidth: 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 withads-fillonly when you also want the item to take the spare room. ads-center/ads-endtext-align, nothing more. For centring a block, useads-centeredor amargin: autofromads-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.