foundation

The Raster

Why vertical space is counted in text lines, and what that costs.

The Grid counts vertical space in text lines, never in pixels and never in spacing tokens. That one decision explains most of the grid’s API, including the parts that look arbitrary. This page records the reasoning so a future change can be argued against the same arithmetic instead of re-guessed.

Why column counts are a data attribute, not a variable

repeat(var(--n), …) is invalid CSS — a custom property cannot be a track repetition count. So the preset publishes a generated table keyed by [data-raster-columns='N'] and the connect names an entry. Everything else numeric travels as inline custom properties instead. This is not a stylistic choice; it is the shape CSS imposes.

Why auto is the default

The design originally made flow="lines" the default — every row track exactly one line, which is what the field arithmetic assumes. That is correct for deliberately placed fields and catastrophic for ordinary content.

With uniform one-line tracks, an auto-placed item that declares no rowSpan gets exactly one track. A cell containing a paragraph is therefore clamped to 24px and paints over the cell below it. The home page rendered as a stack of overlapping 24px slivers.

So lines is correct for placed fields and unusable for flowed content, and the default is the forgiving one.

Why the default gutter is one line, not 16px

In flow="lines" and flow="fields" the vertical gutter is a track, not a gap — it is structurally one line and cannot be anything else without breaking the field arithmetic. A 16px default would therefore be 16px in auto and 24px in the exact modes, so a grid’s gutter would change the moment you switched its flow. One line is the only value that is the same in all three.

It is also the conventional desktop gutter: Bootstrap’s default is 1.5rem, Material’s desktop gutter is 24px.

The same argument is why spacing inputs take a line count rather than a token. On the current scale only xl (24px, one line) and 7xl (48px, two lines) divide evenly into the line. Every other token is a fraction of one. “Use a token and stay on the raster” therefore requires knowing a coincidence between two independently generated scales — SPACING_UNIT_PX = 4 and RASTER_LINE_PX = 24 — and re-checking it whenever either moves. A count cannot drift; it resolves as calc(var(--raster-line) * N).

The field formula

In Müller-Brockmann’s field grid, margins define a type area, the type area divides into fields, a field is an integer number of text lines tall, and the vertical gutter between two fields is exactly one line.

An item spanning S fields is therefore

height(S) = S·k + (S − 1) lines

where k is lines per field: S fields of k lines each, plus the S − 1 one-line gutters they swallow. That is why a two-field span is eleven tracks at lines={5} rather than ten — and why rowSpan is converted by the connect rather than passed through to grid-row.

The honest summary

The horizontal raster is always exact; the vertical raster is exact only in lines and fields. A page using the default gets Müller-Brockmann’s columns and a baseline-quantised minimum, not full field geometry. Opting into the real thing is a per-grid decision, and for content whose height depends on how text wraps it is the wrong decision.

How it’s wired

Three transports carry the geometry, and each exists because the previous one cannot express the next thing:

  1. A generated attribute table — [data-raster-columns='N'] and its per-breakpoint siblings, emitted into globalCss by the preset. This is the only way to get a track repetition count out of a value, per the section above.
  2. Input slots plus a stylesheet reset — every other number (lines, gutter, margin, maxWidth, spans) travels as an inline custom property, one per breakpoint slot. The [data-raster], [data-raster-area] rule resets every var × slot to initial in the stylesheet, so an unset slot is guaranteed invalid rather than inherited from an ancestor grid. A connect then emits only the slots that were actually set, which is what keeps a grid root’s inline style down to a handful of declarations.
  3. Recipe variants — the things that are genuinely enumerable: flow, page, align, alignItems, overflow, rhythm. A variant is also how the row pitch is published: --raster-row-pitch and --raster-row-gutter-track are set by the flow variant on the grid root, and every item’s placement arithmetic reads them. That is why a GridItem no longer takes a flow of its own — it cannot disagree with the grid it is in.

Further reading

The derivation of the numbers — why the line is 24px, why the interior resolution stays 4px, and the one control size that had to be retuned to fit — is in docs/guides/raster-line-unit.md in the repository.

The decision record for the September 2026 review of this system — every finding, the alternatives that were rejected, and how to revert each decision — is in docs/guides/grid-system-review-2026-09.md.