Subgrid: Nested Grids That Share the Parent's Tracks
A nested grid normally invents its own tracks. Subgrid makes it reuse the parent's instead, which fixes card rows, but also removes the implicit grid and lets padding leak into parent sizing.
Nested grids are where CSS grid stops being tidy. A card sets `display: grid` on itself so its own children line up, and in doing so it becomes an independent grid: nested grids are independent of the parent grid and of each other, they do not take their track sizing from the parent, and that makes it difficult to line up nested grid items with the main grid.[1] Three cards in a row end up with three unrelated row structures, and the usual escapes are fixed heights, `min-height` guesses, or abandoning grid inside the card altogether. Subgrid removes that trade-off by letting a nested grid adopt the tracks of its parent instead of creating a new track list of its own.[1]
It is a value, not a mode
`subgrid` is not a `display` value and not a separate layout model; it is a value used in place of a list of grid tracks, and what it buys you is track sizing, templates and names shared with nested grids.[2] Everything else about the element is unchanged, because the subgrid itself lays out as an ordinary grid item in its parent grid.[3] The requirement that catches people is that the element must also be `display: grid`, since passing tracks down is grid container behaviour, so every level of the chain has to opt in explicitly.[2]
The card row, measured
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.card {
display: grid;
grid-row: span 3; /* claim three rows of the parent */
grid-template-rows: subgrid; /* ...and reuse them */
gap: 8px;
}I built that row with three cards of deliberately different content lengths and measured y-offsets in Chromium 153.0.8010.12 at a 1000px viewport: all three headings sat at 11px and all three prices at 147px, which is the alignment you get when a nested grid reuses its parent's tracks.[1] Swapping the cards to `grid-template-rows: auto auto auto` kept the headings aligned at the top but scattered the prices to 395px, 404px and 410px. The first row of every card lines up by default; it is the second and third rows that only subgrid can tie together, because only subgrid makes them the parent's rows. That is the whole value proposition, and it has a price: the card is now coupled to the number of rows it spans, and `grid-row: span 3` is a promise about the parent's track structure.
Line numbers restart at the subgrid edge
.parent {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
}
.sub {
display: grid;
grid-column: 2 / 4;
grid-template-columns: subgrid;
grid-template-rows: subgrid;
}
.sub > .first { grid-column: 1; }Numeric line indices inside a subgrid are scoped to the subgrid, counting from the first line of the subgrid rather than the first line of the parent grid.[3][1] I placed a probe at `grid-column: 1` inside a subgrid that itself started at the parent's column line 2, and it landed at x=341 — the subgrid's own left edge, offset only by its 1px border. That is what makes a subgrid component portable: you can lay out a component that may be placed in different positions on the main grid, knowing the line numbers on the component will always be the same.[1] Line names survive the trip too, because names declared on the parent are passed into the subgrid.
The trap: a subgrid has no implicit tracks
Grid's usual escape hatch is the implicit grid, where one item too many makes the container quietly grow a row, and a subgrid cannot do that because its explicit grid is the set of lines it shares with its parent and each item's grid area is clamped to that explicit grid rather than being allowed to extend it.[3] If you need to autoplace items and do not know how many you will have, subgrid will prevent additional rows from being created to hold them.[1] I autoplaced eight children into a subgrid spanning two columns and one row, and all eight landed in that single row — two columns of four, overlapping, inside a subgrid 10px tall — which is the clamping the specification describes for a `span 1` subgrid holding an item at `grid-column: 2 / span 3`.[3]
The practical rule that follows is to subgrid only the axis you actually need to align on. Drop `grid-template-rows: subgrid`, leave the rows as ordinary tracks, and the regular creation of implicit tracks returns — they simply will not line up with the parent's.[1]
Gaps and padding are not what they look like
Any `gap`, `column-gap` or `row-gap` on the parent is passed into the subgrid, and the subgrid can override it.[1] With a 24px gap across four 182px tracks, a subgrid spanning two of them and setting both gaps to `0` gave its items 194px each: the gap space reappears inside the items rather than between them. Padding behaves the same way, and more surprisingly, since the sum of the subgrid's margin, padding, scrollbar gutter and border at each edge is applied as an extra layer of margin to the items at those edges, and that layer participates in sizing the parent's tracks.[3] A subgrid with 40px padding wrapping a 10px-tall item made the parent's row 90px tall, and a sibling in the next column started at that same row top. Padding on a subgrid is not internal breathing room; it is a claim on the parent's layout.
Passing a macro grid two levels down
.device {
display: grid;
grid-template-columns:
[full-start] 1rem [main-start] 1fr 1fr 1fr [main-end] 1rem [full-end];
}
.device > .app { display: grid; grid-column: full-start / full-end; grid-template-columns: subgrid; }
.app > .section { display: grid; grid-column: main-start / main-end; grid-template-columns: subgrid; }
.app > .section img { grid-column: full-start / full-end; }Named lines pass through every level, which is what turns a designer's page-level macro grid into something a deeply nested element can still address: the names from the outer grid become available to children of the inner one instead of only to the inner one itself.[2] In my test the image two levels down resolved `main-start` to the parent's 10px inset exactly, with no negative margins and no magic numbers. The cost is that the chain has to be unbroken, because a break in the `display: grid` plus `subgrid` chain stops the names there.[2]
Support
Subgrid is Baseline newly available, with all three major engines shipping it by September 15, 2023.[2] The compatibility data puts Chrome at 117, Firefox at 71 and Safari at 16.[4] For progressive enhancement, web.dev recommends asking the browser whether it understands subgrid as a value for template columns or rows inside `@supports`.[2] In Chromium 153 that check, `CSS.supports('grid-template-columns', 'subgrid')`, returned `true`, while `repeat(2, subgrid)` returned `false` because `repeat()` in a subgridded axis only carries line names.
When it is the wrong tool
Reach for a plain nested grid instead when the alignment you want lives entirely inside one component, when the component must grow rows from content of unknown length, or when its parent is itself a subgrid with a fixed span, in which case your span gets clamped and the surplus items pile into the final track.[3] The mental model that made subgrid click for me: an ordinary nested grid is a new coordinate system, while a subgrid is a view onto the parent's coordinate system with its origin moved to wherever the element sits.
Sources
[1] MDN: Subgrid (CSS grid layout guide) — https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout/Subgrid
[2] web.dev: CSS subgrid — https://web.dev/articles/css-subgrid
[3] CSS Grid Layout Module Level 2 (W3C ED) — https://drafts.csswg.org/css-grid-2
[4] MDN browser-compat-data: grid-template-columns (subgrid support) — https://github.com/mdn/browser-compat-data/blob/main/css/properties/grid-template-columns.json