LayoutGENERALSPEC-EVOLVING

Grid: Two Dimensions at Once

The container declares tracks and lines; items are placed into the cells between them. Rows can finally align to columns, because the layout — not the content — owns the sizes.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

When is a two-dimensional layout the right model, and what is 1fr actually a fraction of?

The user intent

Someone is building a page shell: header, sidebar, main, footer. They want the sidebar and the main column to share a baseline, the footer to sit at the bottom even on a short page, and the whole thing to become one column on a phone.

The obvious build

Nest flex containers: a column for the page, a row for the middle, a column inside each of those. It works, so grid is just a different syntax for the same thing.

Why it breaks

Nested flex containers cannot align across siblings. The third item in row one has no relationship to the third item in row two, so a card grid built from flex rows has columns that drift as content changes.

How it breaks in a real browser
  • Nested flex containers cannot align across siblings. The third item in row one has no relationship to the third item in row two, so a card grid built from flex rows has columns that drift as content changes.
  • grid-template-columns: 1fr 1fr overflows its container the moment one cell holds a long word, because 1fr means minmax(auto, 1fr) and auto as a minimum is the min-content size (Intrinsic Sizing and the Automatic Minimum).
  • A responsive card grid built with flex-wrap leaves an awkward last row: the remaining items grow to fill it and are visibly wider than the rest. Grid's auto-fill keeps empty tracks; auto-fit collapses them. Neither is "the correct one" — they are different intentions.
  • Each level of nesting is another intrinsic-measurement pass, so a four-deep flex shell measures the same text several times to place one box (Flexbox: One Axis at a Time).
  • Moving an item with grid-row / grid-column changes only where it paints. DOM order, tab order and reading order stay where the markup put them, and grid-auto-flow: dense can reorder items visually with no markup change at all.
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • A grid container defines tracks — rows and columns — separated by numbered lines. Items are placed into areas bounded by those lines. The container owns the track sizes; items do not negotiate them the way flex items negotiate main-axis space.
  • The explicit grid is what grid-template-rows / grid-template-columns / grid-template-areas declare. Anything placed outside it creates implicit tracks, sized by grid-auto-rows / grid-auto-columns (default auto). A surprise extra row is almost always an implicit track.
  • Track sizing runs in phases: resolve intrinsic sizes (auto, min-content, max-content, fit-content()) against the items in each track, then distribute any remaining space to the flexible (fr) tracks in proportion to their factors.
  • fr is a share of *leftover* space, not of the container.** With 200px 1fr 1fr in a 1,000px container, the fr tracks split 800px. And 1fr is shorthand for minmax(auto, 1fr), so its *minimum* is content-derived — which is why minmax(0, 1fr) is the version that actually allows shrinking.
  • Auto-placement walks items in DOM order and drops each into the first available cell. grid-auto-flow: row (default) fills across then down; column fills down then across; dense back-fills earlier holes, which changes visual order without changing the DOM.
  • repeat(auto-fill, minmax(16rem, 1fr)) asks the browser to fit as many tracks as it can. `auto-fill` keeps empty tracks; `auto-fit` collapses them to zero, so the remaining items stretch. The difference is only visible when items are fewer than the tracks that fit.
  • subgrid lets a nested grid adopt its parent's tracks, so a card's internal rows can align with every other card's — the one thing nesting could never do before.

What this makes the browser do

And which of it is avoidable.

  • Track sizing is a multi-pass algorithm over the items in each track, and intrinsic tracks (auto, min-content, max-content) require measuring item content at both min-content and max-content sizes before any space can be distributed.
  • Fixed and fr tracks are cheap by comparison: their sizes do not depend on content, so no measurement pass is needed for them at all. A grid of minmax(0, 1fr) tracks is meaningfully less work than a grid of auto tracks.
  • One grid container replaces several nested flex containers, and each level removed is a measurement pass removed. This is a real reason to prefer grid for page-level structure, not an aesthetic one.
  • grid-template-areas costs nothing extra at layout time — it is a different way of writing line placement, resolved during style, not a separate algorithm.
  • Very large grids are still large: a thousand items means a thousand boxes to size, place and paint whether or not they are on screen. That is a virtualization or containment problem, not a grid problem (content-visibility).

Lines, tracks, areas

The vocabulary is small and doing it once removes most of the confusion. Lines are numbered boundaries, starting at 1 on the start edge and countable backwards with negative numbers from the end edge. Tracks are the space between two adjacent lines — the rows and columns. An area is a rectangle bounded by four lines, and it is what an item is placed into.

Note what is not in that list: the item. Grid places items into a structure that already exists, which is the fundamental difference from flexbox, where items negotiate the space between themselves. It is also why grid-column: -1 is useful — the end line is addressable, so "span to the last column" does not need to know how many columns there are.

grid-template-columns: 200px minmax(0, 1fr) 200px;   /* three tracks */
grid-template-rows:    auto minmax(0, 1fr) auto;
gap: 1rem;

  line 1        line 2                line 3        line 4
  |             |                     |             |
  v             v                     v             v
1>+-------------+---------------------+-------------+
  |                  header  (1 / 1 / 2 / -1)       |   <- spans all columns
2>+-------------+---------------------+-------------+
  |   sidebar   |        main         |    aside    |
  |             |                     |             |
3>+-------------+---------------------+-------------+
  |                  footer  (3 / 1 / 4 / -1)       |
4>+-------------+---------------------+-------------+
  <- 200px  ->  <-- leftover space --> <- 200px  ->
                    that is what 1fr divides

  the same thing, named:

    grid-template-areas:
      "head head head"
      "side main aside"
      "foot foot foot";
    .header { grid-area: head; }

  fr, precisely:   1fr == minmax(auto, 1fr)
                   min is AUTO (content!) -> a long word widens the track
                   minmax(0, 1fr) -> min is 0 -> the track can actually shrink

The responsive gallery, without a single media query

GENERALauto-fill, auto-fit and minmax() are Grid Level 1 and interoperable. The @container query in the same example is newer: it is supported in all three current engines but absent from older Safari and Chromium versions still in the field, so it needs a working fallback rather than being assumed (Container Queries).

The repeat(auto-fill, minmax(...)) idiom is the clearest demonstration of what grid is for. You state a minimum comfortable track size and a maximum share of leftover space; the browser computes how many tracks fit and reflows as the container changes. There is no breakpoint to keep in sync with a design token.

The auto-fill versus auto-fit choice is the one thing worth being deliberate about, and it only matters when there are fewer items than tracks that would fit — which, inconveniently, is exactly the case that shows up in an empty state or a filtered result and not in the design mock.

One declaration, every viewport
1.gallery {
2 display: grid;
3 grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
4 gap: 1rem;
5}
6
7/* auto-fill: keeps empty tracks. two cards in a six-track row stay card-sized. */
8/* auto-fit: collapses empty tracks. two cards stretch across the whole row. */
9
10/* the cards themselves stay self-contained */
11.card {
12 display: grid;
13 grid-template-rows: auto 1fr auto; /* title, body, actions */
14 min-inline-size: 0; /* let long content shrink the card */
15}
16
17/* respond to the space the component has, not the space the window has */
18@container (inline-size < 24rem) {
19 .card { grid-template-rows: auto auto auto; }
20}

The min-inline-size: 0 on the card is the same automatic-minimum rule as in flexbox, arriving through 1fr's auto minimum instead. Grid items have min-width: auto too.

Grid or flex — a question about who owns the size

The dimension count is the usual way this choice is taught and it is the less useful half of the answer. The sharper question is authority: should the container decide the sizes, or should the content? Grid gives the container authority. Flexbox lets content push back.

Once you ask it that way, the mixed cases resolve themselves. A page shell is grid, because the sidebar width is a design decision. A toolbar is flex, because the title should take whatever the buttons leave. A card is often both: grid for its internal rows, flex for the row of actions at the bottom.

Which layout mode does this need?

You have a container and some children. Who decides the children's sizes?

Grid — the container decides, in two axes

when Page shells, dashboards, galleries, forms with aligned labels — anywhere rows must align to columns

cost Content that does not fit must be handled explicitly, and the track list is a second place the structure is written down.

Flex — the content decides, along one axis

when Toolbars, chip rows, button groups, anything where one item should absorb the leftover space

cost No alignment across siblings, and nested flex containers each add an intrinsic measurement pass (Flexbox: One Axis at a Time).

Normal flow — nobody decides, it just stacks

when Prose, articles, anything that is a document rather than an interface

cost No control over cross-axis alignment, and margin collapsing to keep in mind (Normal Flow, Overflow and Margin Collapsing).

Subgrid — the *ancestor* decides, through a nested container

when Cards whose internal rows must align with every other card's rows

cost Couples the component to the grid it sits in, so it is no longer self-contained; and support arrived late enough that a fallback is still worth writing.

QuestionFlexboxGrid
AxesOne (main), wrapping into lines on the cross axisTwo, simultaneously
Who sizes tracks or itemsItems negotiate: base size, then grow or shrinkContainer declares tracks; items are placed into them
Cross-sibling alignmentOnly within a line — row two knows nothing about row oneFull: every item in a column shares its track
Responsive without media queriesflex-wrap plus a flex-basisrepeat(auto-fill, minmax(...))
Automatic minimum sizemin-width: auto on items — the classic overflowSame rule, arriving via 1fr = minmax(auto, 1fr)
Where it costs moreNesting: each level re-measures its childrenIntrinsic (auto) tracks: content must be measured before distribution

How to build it

Most important first.

  • Reach for grid when the *layout* should own the sizes in two axes: page shells, card galleries, forms with aligned labels, dashboards. Reach for flex when the *content* should own the sizes along one axis (Flexbox: One Axis at a Time).
  • Use minmax(0, 1fr) rather than 1fr for any track that holds content you do not control. It is the same intent with the automatic minimum removed.
  • Name the layout with grid-template-areas when the shape is meaningful. A rearrangement at a breakpoint then becomes a redrawn ASCII picture rather than four line-number edits that must stay consistent.
  • Prefer repeat(auto-fill, minmax(<min>, 1fr)) for responsive card grids over a ladder of media queries. It responds to the container's actual size, which is the property that matters (Container Queries).
  • Keep DOM order equal to reading order and use placement only to arrange it. If a breakpoint needs a genuinely different reading order, that is a content decision that should be visible in the markup.

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • Placement properties (grid-area, grid-row, grid-column, order) and grid-auto-flow: dense change visual position only. Tab order and screen-reader order follow the DOM, so a rearranged grid puts sighted keyboard users and screen-reader users on two different maps of the same page (Keyboard Operability).
  • dense packing is the sharpest version of this: the browser reorders items to fill holes, so the visual sequence depends on item sizes and can differ between viewport widths. Avoid it anywhere the order carries meaning.
  • A grid is a visual arrangement, not a data table. If the content is tabular, use <table> with proper headers — grid gives no row/column semantics to assistive technology at all.
  • Reflow at 400% zoom is where fixed track lists fail: grid-template-columns: 240px 1fr cannot become one column on its own. An auto-fill/minmax track list or a container query can; a fixed one needs a breakpoint someone has to remember to write (Media Queries Beyond Width).
  • Gaps are not margins and do not collapse, so vertical rhythm in a grid is exactly what you declared. That predictability is what makes grid layouts survive user text-spacing overrides.

What can go wrong

Failure modes
  • The mystery extra row: an item placed on a line that does not exist, generating an implicit track sized auto. The Layout overlay shows implicit lines dashed, which is the fastest way to spot it.
  • 1fr tracks that refuse to shrink because a cell contains a long URL, a <pre>, or a nested scroll container. minmax(0, 1fr) and min-width: 0 on the item are both usually needed.
  • auto-fit used where auto-fill was meant, so a gallery with two items stretches them across the full width and looks broken relative to the four-item case.
  • grid-auto-flow: dense producing a pleasing visual arrangement whose tab order jumps unpredictably around the page.
  • Grid used for a one-dimensional row where content should size itself, producing tracks that fight the content instead of following it.
What can arrive out of order
  • Items appended after first paint are auto-placed into the next available cell, so a grid that receives streamed content reflows as it arrives unless the tracks are content-independent (Visual Stability).
  • Images without intrinsic dimensions inside auto rows resize their track when they decode, moving every item in the rows below them.
Security
  • Track sizes derived from auto are derived from content, so untrusted content controls the layout. One long token can widen a track and push interactive elements to somewhere the user did not expect (Clickjacking and Framing).
  • Visual placement is not access control. An item moved off the visible area with grid placement is still in the DOM, still focusable and still in the accessibility tree.
  • Placement can put a destructive control where a benign one usually sits without any change to the markup, which means visual review and code review can disagree. Anything safety-critical should be positioned by markup order, not by placement.
Misreads
  • "1fr is one fraction of the container." It is one share of the space left after fixed and intrinsic tracks are sized — and its minimum is auto, which is why minmax(0, 1fr) exists.
  • "Grid replaces flexbox." They answer different questions. A toolbar where the title takes the leftover room is a flex problem; a page shell where the sidebar and main column share a baseline is a grid problem.
  • "auto-fit and auto-fill are the same." They differ exactly when there are fewer items than fitting tracks: auto-fill leaves the empty tracks in place, auto-fit collapses them and the items stretch.
  • "Grid means a data table." It is a visual arrangement with no tabular semantics. Assistive technology learns nothing about rows and columns from it.
  • "Placement changes the order." It changes the pixels. Tab order, reading order and copy order all still come from the DOM.

Measuring it, and what changes in the field

How you would see this
  • The Elements panel's grid badge draws line numbers, track sizes, area names and gaps directly over the page. It is the only practical way to see which lines actually exist and which are implicit.
  • In the Computed tab, grid-template-columns shows the used track sizes in pixels rather than the authored value — the fastest way to find the track that refused to shrink.
  • The Performance panel attributes layout to the grid container. Repeated grid layout during a resize usually means a track list depends on content that is itself being re-measured (Layout Thrashing).
Slow device, slow network, large data, old tab
  • On a narrow viewport, auto-fill/auto-fit track lists reflow with no media queries at all; fixed track lists need one per breakpoint and will be forgotten at some size.
  • Inside a resizable panel, the viewport is the wrong thing to query. Container queries let the grid respond to the space it actually has, which is what a sidebar-aware component needs (Container Queries).
  • With hundreds or thousands of items, grid places every one of them whether or not it is visible. content-visibility: auto or virtualization is what changes the cost curve (List Virtualization).
  • In a right-to-left context, column lines start on the right. Line-based placement follows writing direction, so a layout written with logical placement flips correctly and one written with grid-column: 1 and a hardcoded left offset does not (Internationalization).
What this costs
  • Grid gives the layout authority over sizes, which means content that does not fit must be handled explicitly — truncated, wrapped or scrolled. Flex would have let the content push back; grid makes you decide.
  • grid-template-areas is wonderfully readable and duplicates the track structure in a second place, so a rearrangement must update both the areas and the track list.
  • subgrid solves cross-card alignment properly and adds a dependency between a component and the grid it happens to be placed in — the component stops being self-contained.
  • Named areas and explicit tracks are more upfront design than "put flex on it", and they are harder to change ad hoc. That is the point, and it is still a cost.

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALTrack sizing, line-based placement, auto-placement and fr distribution are specified in CSS Grid Layout Level 1 and interoperate across Blink, Gecko and WebKit; this has been stable for years.
  • SPEC-EVOLVINGThe edges are still moving: subgrid reached all three engines much later than the core (Firefox first, Safari next, Chromium last), and masonry-style layout is an active, contested proposal with more than one syntax on the table. Treat both as features to check support for rather than to assume, and expect the masonry syntax in particular to change.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Domains that do not exist yet
  • Software Design — grid-template-areas is a schema for a layout, and it earns the same benefits and costs as any schema: readable intent, and a second place to keep in sync.