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.
When is a two-dimensional layout the right model, and what is 1fr actually a fraction of?
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.
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.
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.
- 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 1froverflows its container the moment one cell holds a long word, because1frmeansminmax(auto, 1fr)andautoas a minimum is the min-content size (Intrinsic Sizing and the Automatic Minimum).- A responsive card grid built with
flex-wrapleaves an awkward last row: the remaining items grow to fill it and are visibly wider than the rest. Grid'sauto-fillkeeps empty tracks;auto-fitcollapses 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-columnchanges only where it paints. DOM order, tab order and reading order stay where the markup put them, andgrid-auto-flow: densecan reorder items visually with no markup change at all.
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-areasdeclare. Anything placed outside it creates implicit tracks, sized bygrid-auto-rows/grid-auto-columns(defaultauto). 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. fris a share of *leftover* space, not of the container.** With200px 1fr 1frin a 1,000px container, thefrtracks split 800px. And1fris shorthand forminmax(auto, 1fr), so its *minimum* is content-derived — which is whyminmax(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;columnfills down then across;denseback-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.subgridlets 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
frtracks are cheap by comparison: their sizes do not depend on content, so no measurement pass is needed for them at all. A grid ofminmax(0, 1fr)tracks is meaningfully less work than a grid ofautotracks. - 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-areascosts 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 shrinkThe responsive gallery, without a single media query
auto-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.
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.
You have a container and some children. Who decides the children's sizes?
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.
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).
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).
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.
| Question | Flexbox | Grid |
|---|---|---|
| Axes | One (main), wrapping into lines on the cross axis | Two, simultaneously |
| Who sizes tracks or items | Items negotiate: base size, then grow or shrink | Container declares tracks; items are placed into them |
| Cross-sibling alignment | Only within a line — row two knows nothing about row one | Full: every item in a column shares its track |
| Responsive without media queries | flex-wrap plus a flex-basis | repeat(auto-fill, minmax(...)) |
| Automatic minimum size | min-width: auto on items — the classic overflow | Same rule, arriving via 1fr = minmax(auto, 1fr) |
| Where it costs more | Nesting: each level re-measures its children | Intrinsic (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 than1frfor 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-areaswhen 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) andgrid-auto-flow: densechange 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). densepacking 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 1frcannot become one column on its own. Anauto-fill/minmaxtrack 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
- 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. 1frtracks that refuse to shrink because a cell contains a long URL, a<pre>, or a nested scroll container.minmax(0, 1fr)andmin-width: 0on the item are both usually needed.auto-fitused whereauto-fillwas 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: denseproducing 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.
- 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
autorows resize their track when they decode, moving every item in the rows below them.
- Track sizes derived from
autoare 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.
- "
1fris one fraction of the container." It is one share of the space left after fixed and intrinsic tracks are sized — and its minimum isauto, which is whyminmax(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-fitandauto-fillare the same." They differ exactly when there are fewer items than fitting tracks:auto-fillleaves the empty tracks in place,auto-fitcollapses 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
- The Elements panel's
gridbadge 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-columnsshows 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).
- On a narrow viewport,
auto-fill/auto-fittrack 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: autoor 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: 1and a hardcoded left offset does not (Internationalization).
- 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-areasis wonderfully readable and duplicates the track structure in a second place, so a rearrangement must update both the areas and the track list.subgridsolves 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
frdistribution 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:
subgridreached 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.
- — Software Design —
grid-template-areasis 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.