Starter
Everything a small team needs to ship a first project, billed by usage with no seat minimum.
Actual CSS
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.
.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 — —
--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.
--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.
.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 — —
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.
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.
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.
.grid-6container — —
.container-query > .grid-6container — —
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.
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.
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 cardscontainer — —
Everything a small team needs to ship a first project, billed by usage with no seat minimum.
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 cardscontainer — —
Guides, references, and runnable examples.
Every release, with migration notes.
Try a component without installing anything.
.grid-4 — compact cards and statscontainer — —
.grid-6 — compact tilescontainer — —
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.
.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.
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 — —
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.
--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.
--gap: var(--space-10)container — —
--gap: var(--space-50)container — —
Both stages keep the same 4 → 2 → 1 chain at the
same thresholds: spacing is independent of density.
--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.
A grid preset balances a collection of peers. Three shapes are not that, and each has its own primitive.
.sidebar-layout, not
--grid-columns: 2fr 1fr.
.switcher. A grid may leave a partial final row; a
switcher never can.
--grid-columns on .grid. Once you set it,
the collapse behavior is yours to write.
container — three peers — never two plus one
Add the stylesheet.
Reach for a primitive.
No build step required.