Skip to content
Abhishek Sawant

Frontend8 Min Read

Subgrid Borrows Its Parent's Tracks

Nested grids can't align with each other because tracks stop at the container that declared them. Subgrid is the one keyword that removes the boundary.

FIG_01 // FRONTEND
Fig. 01 — Subgrid Borrows Its Parent's Tracks

Every card in the row has a headline, a paragraph, and a button. One headline wraps to two lines and the others don’t. Now that card’s paragraph starts a line lower than its neighbours’, its button sits lower than theirs, and a row that should read as one object reads as three objects placed by hand.

So you reach for the fix you know. display: grid on the card, grid-template-rows: auto 1fr auto, and the buttons snap to the bottom edge. Then you look again. The buttons line up. Nothing else does.

That’s the boundary the beginner model leaves out. Tracks belong to the container that declared them, and they stop at that container’s edge. Every card is its own coordinate system, and coordinate systems don’t negotiate.

Subgrid is how you make them negotiate. It’s a smaller feature than its reputation suggests, and it’s mostly one keyword and one number.

Where Cards Stop Lining Up

Here’s the layout. A wrapping card grid, and each card is a three-row grid of its own:

cards.css css
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  gap: 1.5rem;
}

.card {
  display: grid;
  grid-template-rows: auto 1fr auto;
  gap: 0.75rem;
}

This looks like it should work, and it half does. Grid items stretch to fill their cell by default, so all three cards are the same height. The 1fr middle row absorbs the difference, so each card’s third row sits flush against its own bottom edge. Three bottom edges, all at the same y. Three buttons, all in line.

The headline row is the problem. It’s auto, so it’s sized by the content of that card, and each card computes it separately. One card’s headline row is two lines tall, the other two are one line tall, and everything below inherits the offset. Call it the Local Rows Problem: three grids doing the right thing three times, with no mechanism to compare answers.

Nothing you can write inside .card fixes this, because the card has no idea its siblings exist. The information you need — the tallest headline in the row — only exists one level up, in a container that has no rows to put it in.

A Subgrid Stands On Its Parent’s Tracks

grid-template-rows: subgrid

tells a grid item to stop defining its own tracks in that axis and use its parent’s instead. Not a copy of them. The same tracks, for exactly the region the item spans.

Which means the card’s children are no longer laid out against the card. They are placed into the outer grid’s rows, and they participate in sizing those rows. Row one is as tall as the tallest headline across every card sitting on it, because every one of those headlines is contributing to the same track.

The sizing runs both directions, and that’s the part worth holding onto. A subgrid isn’t a passive reader of its parent’s measurements — its own contents push those measurements around, the same way any grid item would. Take the loan out and you’re also on the hook for it.

Fig. 02 — One set of rows, two cards. The headline row is as tall as the taller headline, and the short one leaves a gap.

Notice what that costs. The card with the one-line headline now has empty space under it, because its headline row is sized by somebody else’s copy. That’s the trade, and it’s the right one nearly every time — a row of cards reads as a unit when the eye can follow a straight line across it, and a little air under a short headline is invisible by comparison.

A subgrid doesn’t copy its parent’s tracks. It stands on them.

— Layout_Doctrine // §4

Subgrid is per-axis, which people miss. grid-template-rows: subgrid and grid-template-columns: subgrid are independent, and a card that subgrids its rows can still declare whatever columns it likes internally. Most real uses subgrid one axis and leave the other alone.

The Span Is The Contract

The card version is shorter than the one it replaces:

cards.css css
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  gap: 1.5rem;
}

.card {
  grid-row: span 3;
  display: grid;
  grid-template-rows: subgrid;
  row-gap: 0.75rem;
}

Three things have to be true, and all three are easy to half-do. The card must be a grid container itself — grid-template-rows: subgrid on a block element does nothing at all, in the familiar silent way. It must be a grid item of the grid it’s borrowing from, so no wrapper div in between. And it must span a known number of parent rows, because the span is what decides how many subgridded rows the card gets.

That last one is the only real bookkeeping subgrid adds. span 3 says the card occupies three of the outer grid’s rows, so it has three rows to place children into. Add a badge row to the card six months from now and the span has to grow with it. There’s no error when it doesn’t; the layout goes quietly wrong, which is the failure mode this whole area specialises in.

!

Support: Firefox shipped subgrid in 2019, Safari in 16, and Chromium in 117 in September 2023. That makes it safe across evergreen browsers, but check your own floor if you still support anything older.

One behaviour surprises people the first time a card grid wraps. The outer grid places items in order, so with span 3 the first row of cards occupies rows one to three, the next occupies four to six, and so on. Alignment happens within a band, not across the whole page. Cards in the second row line up with each other and not with the row above.

That’s almost always what you want. The alternative — one tall headline anywhere on the page inflating the headline row for every card — would be a worse layout and a strange thing to ask for. It’s worth knowing before you go looking for a bug that isn’t there.

There is an older trick that gets the same alignment, and it’s worth naming so you can rule it out deliberately rather than by accident:

Breaks

display: contents on the card promotes its children into the outer grid. They align, and the card’s box stops generating — no border, no background, no padding, nothing to hang a hover state on.

Holds

grid-template-rows: subgrid keeps the card as a real element that still paints and still wraps its own content, while its rows come from the parent.

Line Names Come Along

The other half of subgrid is quieter and, for page layout, more useful: a subgrid inherits the parent’s line names across the region it spans. Which turns the named-column article layout from a thing you set up once into a thing nested content can actually use.

page.css css
.page {
  display: grid;
  grid-template-columns:
    [full-start] 1fr
    [text-start] minmax(0, 68ch)
    [text-end] 1fr
    [full-end];
}

A section that spans the full width can adopt those three columns, and everything inside it can then be placed by name instead of by arithmetic:

page.css css
.section {
  grid-column: full-start / full-end;
  display: grid;
  grid-template-columns: subgrid;
}

.section > p {
  grid-column: text-start / text-end;
}

.section > figure {
  grid-column: full-start / full-end;
}

The minmax(0, 1fr)-style discipline from the earlier post on track sizing still applies here — a subgridded track can be blown out by unbreakable content exactly like any other track, because it is any other track. Nothing about subgrid changes how a track decides its size; it only changes who gets to place things on it.

Two mechanics to keep in your head. A subgrid uses its parent’s gutters by default, and setting gap on the subgrid overrides them for that axis — which is what row-gap: 0.75rem on the card is doing above, so the space between rows inside a card doesn’t have to match the space between cards.

And padding is charged to the tracks rather than added around them. Give a subgrid padding on its subgridded axis and the first and last tracks are inset by that amount, because the tracks live in the content box. Cards with identical padding stay aligned with each other, so

Tagged
  • CSS
  • Layout

Related Logs

System Updates

Weekly dispatches,
straight to your terminal.