Grid
.grid is one rule with no breakpoints in it. Columns appear as the
container gets wider and disappear as it narrows, and the number of them is never
written down anywhere — it is a consequence of the available width and a minimum
column size.
When to use it
Use it for a collection of things of the same kind that should line up in columns: cards, stats, thumbnails, a settings menu. Every item gets an equal track, so the second row lines up with the first — which is the difference between a grid and a cluster that happens to wrap.
Exports
A card in a grid track.
Invoices
A card in a grid track.
Members
A card in a grid track.
Webhooks
A card in a grid track.
Audit log
A card in a grid track.
<div class="grid">
<div class="card"><div class="card-body">
<h3 class="card-title">Exports</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Invoices</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Members</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Webhooks</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Audit log</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
</div>
<div class="grid">
<div class="card"><div class="card-body">
<h3 class="card-title">Exports</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Invoices</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Members</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Webhooks</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Audit log</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
</div>
How the one rule works
grid-template-columns: repeat(auto-fit, minmax(min(var(--min, 17rem), 100%), 1fr));
It is worth taking apart, because once you can read it you can write your own.
-
repeat(auto-fit, …)— make as many columns of this size as will fit, and collapse the empty ones so the items stretch to fill the row.auto-fillis the alternative and leaves the empty tracks in place, which is right for a calendar and wrong for a list of cards. -
minmax(X, 1fr)— every column is at leastXand shares the leftover space equally. This is what makes the columns equal rather than content-sized. -
min(var(--min, 17rem), 100%)— the important part, and the part most hand-written versions get wrong. A bareminmax(17rem, 1fr)overflows on any container narrower than17rem, because the minimum is a hard floor. Wrapping it inmin(…, 100%)lets the floor fall to the container's own width when that is smaller, so the grid never pushes the page sideways on a phone.
That min(…, 100%) is the single most useful thing on this page. A grid
that overflows a 320px viewport is nearly always a minmax() with a
fixed floor.
Changing the floor
--min is the only knob. The variants set it and nothing else, so
changing the column floor never changes the gap, the alignment or anything about the
items.
.grid-tight — a 12rem floor
More columns before it wraps. For stats, icons, small tiles.
Exports
A card in a grid track.
Invoices
A card in a grid track.
Members
A card in a grid track.
Webhooks
A card in a grid track.
Audit log
A card in a grid track.
Domains
A card in a grid track.
<div class="grid grid-tight">
<div class="card"><div class="card-body">
<h3 class="card-title">Exports</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Invoices</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Members</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Webhooks</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Audit log</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Domains</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
</div>
<div class="grid grid-tight">
<div class="card"><div class="card-body">
<h3 class="card-title">Exports</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Invoices</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Members</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Webhooks</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Audit log</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Domains</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
</div>
.grid-wide — a 24rem floor
Fewer, wider columns. For cards with a paragraph in them.
Exports
A card in a grid track.
Invoices
A card in a grid track.
Members
A card in a grid track.
<div class="grid grid-wide">
<div class="card"><div class="card-body">
<h3 class="card-title">Exports</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Invoices</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Members</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
</div>
<div class="grid grid-wide">
<div class="card"><div class="card-body">
<h3 class="card-title">Exports</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Invoices</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Members</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
</div>
Any floor you like
--min is a plain custom property, so a floor Deck does not ship needs
no class.
Exports
A card in a grid track.
Invoices
A card in a grid track.
Members
A card in a grid track.
Webhooks
A card in a grid track.
Audit log
A card in a grid track.
Domains
A card in a grid track.
<div class="grid" style="--min:9rem">
<div class="card"><div class="card-body">
<h3 class="card-title">Exports</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Invoices</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Members</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Webhooks</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Audit log</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Domains</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
</div>
<div class="grid" style="--min:9rem">
<div class="card"><div class="card-body">
<h3 class="card-title">Exports</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Invoices</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Members</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Webhooks</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Audit log</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Domains</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
</div>
.grid-2 is the odd one out: it hard-codes a 24rem floor in
grid-template-columns rather than setting --min, so it is
identical to .grid-wide and cannot be adjusted with
--min afterwards. Two names for one behaviour, one of which composes
and one of which does not — recorded in FINDINGS.md.
Fixed column counts
.grid-fixed-2, -3 and -4 set an exact number
of equal columns and keep it at every width. They do not collapse on a phone.
Exports
A card in a grid track.
Invoices
A card in a grid track.
Members
A card in a grid track.
<div class="grid grid-fixed-3">
<div class="card"><div class="card-body">
<h3 class="card-title">Exports</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Invoices</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Members</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
</div>
<div class="grid grid-fixed-3">
<div class="card"><div class="card-body">
<h3 class="card-title">Exports</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Invoices</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Members</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
</div>
Reach for these only when the count is part of the meaning — a 2×2 comparison,
a three-up pricing table where the middle one is the recommended plan. For anything
else the auto-fit grid is better, because it is right at widths you did not think
about. A .grid-fixed-4 on a phone is four 70px columns.
Gap
Same --gap as everything else, defaulting to
1rem, so the
spacing scale applies unchanged.
Exports
A card in a grid track.
Invoices
A card in a grid track.
Members
A card in a grid track.
Webhooks
A card in a grid track.
<div class="grid grid-tight stack-1">
<div class="card"><div class="card-body">
<h3 class="card-title">Exports</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Invoices</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Members</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Webhooks</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
</div>
<div class="grid grid-tight stack-1">
<div class="card"><div class="card-body">
<h3 class="card-title">Exports</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Invoices</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Members</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
<div class="card"><div class="card-body">
<h3 class="card-title">Webhooks</h3>
<p class="text-muted">A card in a grid track.</p>
</div></div>
</div>
Cards that respond to their track
src/18-container.css makes a .cq child of a grid its own
query container. That closes the loop: the grid decides how wide each track is, and
then each item lays itself out according to the track it landed in — with no media
query involved at either step.
Sized by its track
Not by the window
<div class="grid grid-tight">
<div class="cq">
<div class="card card-flex">
<div class="card-body"><h3 class="card-title text-cq">Sized by its track</h3></div>
</div>
</div>
<div class="cq">
<div class="card card-flex">
<div class="card-body"><h3 class="card-title text-cq">Not by the window</h3></div>
</div>
</div>
</div>
<div class="grid grid-tight">
<div class="cq">
<div class="card card-flex">
<div class="card-body"><h3 class="card-title text-cq">Sized by its track</h3></div>
</div>
</div>
<div class="cq">
<div class="card card-flex">
<div class="card-body"><h3 class="card-title text-cq">Not by the window</h3></div>
</div>
</div>
</div>
Classes
Generated from src/04-layout.css by tools/docs/extract.mjs.
| Class | Layer | What it does | Source |
|---|---|---|---|
.grid |
deck.layout |
Auto-fitting grid: no breakpoints needed, it responds to its own width | 04-layout.css:102 |
.grid-2 |
deck.layout |
Two-ish columns: an auto-fit grid with a 24rem floor. Identical in effect to .grid-wide, but it writes grid-template-columns directly rather than setting --min, so --min cannot adjust it afterwards. Prefer .grid-wide. | 04-layout.css:111 |
.grid-fixed-2 |
deck.layout |
Exactly two equal columns at every width, including a phone. Only for a count that is part of the meaning — a 2x2 comparison. It does not reflow, so it does not satisfy the reflow criterion on its own. | 04-layout.css:121 |
.grid-fixed-3 |
deck.layout |
Exactly three equal columns at every width. See .grid-fixed-2. | 04-layout.css:123 |
.grid-fixed-4 |
deck.layout |
Exactly four equal columns at every width. On a phone that is four 70px columns; reach for .grid with a --min instead. | 04-layout.css:126 |
.grid-tight |
deck.layout |
Narrows the auto-fit column floor to 12rem, so the grid packs more columns in before it wraps. | 04-layout.css:114 |
.grid-wide |
deck.layout |
Widens the auto-fit column floor to 24rem, so the grid packs fewer, wider columns before it wraps. For cards with a paragraph in them. | 04-layout.css:117 |
Tokens
| Token | Live | Declared value |
|---|---|---|
--space-1 |
.25rem |
|
--space-3 |
.75rem |
|
--space-4 |
1rem |
|
--space-6 |
1.5rem |
Accessibility
-
A CSS grid is not a table. It has no row or column semantics, so
a screen reader reads the items in DOM order with no notion of position. If the
row and column a value sits in carries meaning, it belongs in a
<table>. -
Reading order is DOM order, and the auto-fit grid never changes
it. That is a real advantage of this pattern over one that places items explicitly:
there is no
grid-areato get out of step with the markup. -
Do not use
grid-auto-flow: densefor a list of links. It reorders items visually to fill holes, leaving the tab order following the markup and the eye following the screen. It is fine for a photo wall and wrong for anything interactive. -
A grid item's minimum size is its content. A long unbroken string
— a URL, a hash, a wide code block — makes its track wider than
1frand pushes the page sideways. Addmin-inline-size: 0to the item, which is exactly what.splitdoes for the same reason. -
Reflow. The auto-fit grid satisfies WCAG's reflow criterion for
free: at 320px or 400% zoom it becomes one column and nothing is lost. The
.grid-fixed-*variants do not, and are the one part of this page to use carefully. - Cards in a grid need their own headings. The grid supplies no structure, so a page of twelve cards with no headings is twelve unlabelled regions.
Right to left
Grid columns follow the inline direction, so items fill from the right under
dir="rtl" with no extra rules and no change to the markup order.
الأول
الثاني
الثالث
<div dir="rtl" class="grid grid-tight">
<div class="card"><div class="card-body"><h3 class="card-title">الأول</h3></div></div>
<div class="card"><div class="card-body"><h3 class="card-title">الثاني</h3></div></div>
<div class="card"><div class="card-body"><h3 class="card-title">الثالث</h3></div></div>
</div>
<div dir="rtl" class="grid grid-tight">
<div class="card"><div class="card-body"><h3 class="card-title">الأول</h3></div></div>
<div class="card"><div class="card-body"><h3 class="card-title">الثاني</h3></div></div>
<div class="card"><div class="card-body"><h3 class="card-title">الثالث</h3></div></div>
</div>
Reduced motion
Nothing animates. Reflowing as the container changes width is layout, not a
transition, so prefers-reduced-motion has no bearing on it.
Printing
src/99-print.css sets .grid { display: block } and gives
every child after the first margin-block-start: 4mm. Grid tracks and
paged media do not get on — an item taller than the remaining page cannot break out
of its track — so the grid is unwound into a plain block flow and the spacing is
restored with a margin. It is the one place in Deck where a margin is the right
answer.
Overriding it
/* One grid, no layer needed */
<div class="grid" style="--min:9rem;--gap:var(--space-6)">
@layer app.components {
/* Keep empty tracks — right for a calendar, wrong for cards */
.grid-calendar {
grid-template-columns: repeat(auto-fill, minmax(min(6rem, 100%), 1fr));
}
/* A first item that spans the full width */
.grid > .span-all { grid-column: 1 / -1; }
}
grid-column: 1 / -1 on a child is the escape hatch worth remembering: it
spans every track whatever the current column count turns out to be, so a full-width
header inside an auto-fit grid needs no breakpoint either.
When not to use it
-
Not for content and a sidebar. That is
.split, which has one flexible column and one fixed rail. A grid gives every column an equal share, which is not what a sidebar is. -
Not for a row of buttons. Use
.cluster. A grid would give each button an equal track and stretch them all to the widest one. -
Not for tabular data. Use
.table. A grid loses the header association and the row/column semantics that make a table readable to a screen reader. -
Not when the item count is part of the design and small. Two
things side by side that must stay side by side is
.grid-fixed-2, or a.barif they are not equal. -
.grid-fixed-*is not responsive. Four fixed columns at 320px is four 70px columns. If you find yourself adding a media query to undo a fixed grid, you wanted the auto-fit one with a--min.