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.

Resize the window. Nothing here knows what a breakpoint is.

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-fill is 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 least X and 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 bare minmax(17rem, 1fr) overflows on any container narrower than 17rem, because the minimum is a hard floor. Wrapping it in min(…, 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.

A 9rem floor, inline

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.

Three columns, at every width, including 320px

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.

.stack-1 on a grid: the step classes only set --gap, so they compose

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.

Each item queries its own column

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.

Classes, generated from the stylesheet source
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

Design tokens this component reads
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-area to get out of step with the markup.
  • Do not use grid-auto-flow: dense for 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 1fr and pushes the page sideways. Add min-inline-size: 0 to the item, which is exactly what .split does 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.

First in the markup is first from the right

الأول

الثاني

الثالث

<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 .bar if 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.