Actual CSS

Grid density

One question decides which class you want. Does the item width determine the column count, or does the column density?

Every stage below is resizable — drag its bottom-right corner. The readout follows the stage, never the window. That is the contract: these grids react to the space they are given, not to the viewport.

1. Item width is the contract — .grid

.grid fits as many columns as --grid-min allows. An incomplete final row is expected: the items are independent, so nothing is wrong with a row of two under a row of four.

container

1
2
3
4
5
6

--grid-min: 12rem here. Raise it for wider items, lower it for denser ones. It is the only knob, and it belongs to .grid alone.

Pitfall — --grid-min is not a .grid-N hook

It tunes .grid only. The presets deliberately ignore it: their subdivision runs on @container thresholds, which cannot resolve a custom property, so a hook honored in the unwrapped baseline would fall silent the moment you added a query container. Item width is .grid's contract; column density is .grid-N's.

2. Density is the contract — .grid-N

.grid-4 says this content suits four columns. It never exceeds four, never overflows, and keeps every item the same width — with no wrapper and nothing to configure.

container

1
2
3
4
5
6

Drag it slowly: the count walks down one column at a time. With six items that means you pass through three columns — a row of three above a row of three is fine — but a five-column state would have left a lone sixth item. That is what the next section removes.

Pitfall — a bare preset is not a fixed N-column grid

If the exact track structure is the contract — a comparison matrix, a form row where an empty cell must stay empty — a preset is the wrong tool. It will collapse. Use --grid-columns on .grid and own the responsive behavior yourself.

3. Balanced subdivision — inside an actual-container container

Place the preset inside a container named actual-container — the .container-query helper, or a region of your own that declares container: actual-container / inline-size — and it only ever enters a divisor of N. Both stages hold six items. Drag either and compare.

Bare — .grid-6

container

1
2
3
4
5
6

Wrapped — .container-query > .grid-6

container

1
2
3
4
5
6

The chains are 2 → 1, 3 → 1, 4 → 2 → 1, and 6 → 3 → 2 → 1. Intermediate counts are skipped on purpose. The container has to be an ancestor: an element cannot query its own size without taking inline-size containment, which would collapse it inside a .cluster or any shrink-to-fit box.

Forgetting the container costs you the exact chain and nothing else — the bare preset on the left is still responsive and still overflow-safe.

Pitfall — where the query container may sit

The container takes inline-size containment, so its contents stop contributing to its own inline size. On a normal block-level wrapper that is free. On a shrink-to-fit box — a .cluster child, a .topbar child, a float, an inline-block, a width: fit-content element — it collapses to roughly nothing.

Nested containers resolve to the nearest ancestor. A preset inside an inner container measures that inner box, not the page.

4. N is a statement about your content

The presets are a density scale, not a column counter. Pick the one your items are sized for; the framework then guarantees a clean collapse of that density. All four stages are wrapped and start at the same width.

.grid-2 — large cards

container

Starter

Everything a small team needs to ship a first project, billed by usage with no seat minimum.

Scale

Dedicated capacity, priority support, and audit logs for larger organisations.

The cards are equal width because the grid says so, and equal height because grid items stretch by default. Aligning the two actions on top of that takes one utility: .margin-block-start-auto on the button pushes it to the bottom of its .stack, so a two-line summary next to a one-line summary no longer staggers them.

.grid-3 — standard cards

container

Documentation

Guides, references, and runnable examples.

Changelog

Every release, with migration notes.

Playground

Try a component without installing anything.

.grid-4 — compact cards and stats

container

1 248Projects
99.9%Uptime
17Open issues
4.8Rating

.grid-6 — compact tiles

container

Box A
Box B
Box C
Box D
Box E
Box F

Three shared thresholds drive all of them — 28rem opens the first horizontal density, 48rem opens three columns, 64rem opens the dense four and six. They are deliberately not tuned per preset to land the same item width: that would erase the information N carries. At 64rem .grid-6 gives roughly 160px per tile, which is the point of choosing it. If your items need 250px, you wanted .grid-4.

Pitfall — the chain balances the density, not your item count

.grid-6 guarantees that every column count divides six. It cannot know how many children you passed it. Eight items still give 6 + 2, and three columns give 3 + 3 + 2.

For a paginated collection, pick a page size that suits the density — 12, 20, and 24 divide well. A partial final page is normal and needs no fixing.

5. No gap — flush mosaics

The presets read --gap like every other layout primitive, so a flush grid is just .gap-none. Cells touch, the density chain is untouched, and items still share one width — which is what makes a seamless mosaic possible at all.

container

01
02
03
04
05
06
07
08
09
10
11
12

Twelve cells, so every state in the 6 → 3 → 2 → 1 chain divides the collection exactly — no partial row at any width. That is the pairing to aim for: choose a collection size that is a multiple of the density you declared.

6. Density — --gap and the spacing utilities

Set --gap to change the rhythm of a grid. It is the same relay .stack, .cluster, and .switcher read, so one declaration retunes a whole composition rather than one class.

Tight — --gap: var(--space-10)

container

1
2
3
4
5
6
7
8

Airy — --gap: var(--space-50)

container

1
2
3
4
5
6
7
8

Both stages keep the same 4 → 2 → 1 chain at the same thresholds: spacing is independent of density.

Note — --gap is the only gap knob

There is no .gap-sm / .gap-lg scale. A grid sizes its tracks from --gap, so setting the gap property on its own would let spacing and density disagree. Override --gap instead: it is exact at every width, and it retunes the whole composition, since .stack, .cluster, and .switcher read the same relay.

.gap-none stays safe at every width, and inside .container-query the column count is fixed per step, so any form is exact.

7. When none of them is the answer

A grid preset balances a collection of peers. Three shapes are not that, and each has its own primitive.

container three peers — never two plus one

01 / Install

Add the stylesheet.

02 / Compose

Reach for a primitive.

03 / Ship

No build step required.