Subgrid
The row of cards nobody could align, and the loveliest fallback story in modern CSS.
Six years ago, the week Grid shipped, I wrote a fairly giddy post about it and put one complaint at the bottom. The thing where a nested grid lines up with its parent’s tracks had been deferred to a second level of the spec, and I said it was the omission I expected to be annoyed by, because “the card’s internals line up with the page grid” is a thing designers ask for constantly.
I was annoyed by it for about a year. Then I stopped, and I think that’s worth noting on its own, because I didn’t stop wanting it. I stopped noticing that I wanted it, and started reaching for the workaround automatically, the way you stop noticing a door that sticks.
The row of cards
The concrete case, which is on approximately every site I have ever built.
Three cards side by side. Each has an image, a heading, a paragraph, and a button at the bottom. The headings are different lengths, because content comes from a CMS and one of them is four words and one of them is 11.
What the designer drew, and what the client will notice within about four seconds: the headings all start on the same line, the paragraphs all start on the same line, and the buttons are all on the same line.
What you get: three independent cards, each laying itself out from the top, each with its own internal rhythm, none of them agreeing about anything.
The reason is structural rather than a bug. Each card is its own grid, its own formatting context, and the rows inside card one have no relationship of any kind with the rows inside card two.
Each card is its own grid, so the rows inside card one have no relationship whatsoever with the rows inside card two. That's not a bug, it's the whole containment model.
What we all did instead
Four workarounds, in ascending order of shame, and I have shipped all four.
A min-height on the heading, sized for the longest title anybody could imagine. Works until somebody writes a longer one, and puts an awkward gap under every short one.
Clamping the heading to two lines, which aligns everything beautifully and achieves it by hiding the client’s content, which is a decision I have made without asking them.
Flexbox on the card with margin-top: auto on the footer, which pushes the button to the bottom of its own card. Combined with grid giving you equal-height cards, that genuinely aligns the buttons, and it’s the best of the four. It does nothing whatsoever for the headings and paragraphs in the middle, and the middle is most of the card.
And measuring in JavaScript. There were plugins for this. I used one for years. It runs on load and on resize, it fights with web fonts arriving, and if the script is late the layout visibly settles.
What subgrid does
The child grid stops being its own thing and adopts its parent’s tracks on whichever axis you name.
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: 1.5rem;
}
.card {
grid-row: span 4; /* image, heading, body, footer */
display: grid;
grid-template-rows: subgrid; /* use the parent's rows, not my own */
gap: 0;
}
Every card now places its four parts into the same four rows of the outer grid. The outer grid sizes each of those rows to fit the tallest content across all the cards. So the headings align because they’re all in row two, and row two is as tall as the tallest heading.
No heights, no clamping, no JavaScript, no measuring. And it survives the client pasting a very long title, because the row simply grows and everything stays aligned.
The same trick does the thing I’ve wanted on forms for years: labels and inputs aligning across a whole form even when they’re wrapped in per-field components, which otherwise needs either a flat DOM or a fixed label width.
The span you have to declare
The bit that catches everybody, including me for about 20 minutes.
grid-template-rows: subgrid on its own doesn’t do anything useful, because the card also has to occupy the rows it wants to subdivide. Hence grid-row: span 4. The card is claiming four rows of the parent, and then subgrid says “and my children go into those four.”
Which introduces the one real constraint: you have to know how many parts the component has. If a card sometimes has an image and sometimes doesn’t, the span isn’t constant, and you’re either always reserving the row or reaching for a relational selector to change the span. That’s manageable and it’s a genuine wrinkle for CMS-driven content where every part is optional.
Also worth knowing: gaps inherit from the parent by default, which is usually what you want and is occasionally surprising. And you only have to subgrid one axis; columns and rows are independent, and rows are the one you’ll want most.
Support, and the nicest fallback story going
Firefox has had this since 2019, and was on its own for nearly three years. Safari got it in the fall. Chrome is the remaining one and has it working behind a flag, and the signals are that it’s close.
Which would normally make this a “watch this space” post, except the degradation here is the best I’ve encountered.
If a browser doesn’t support the value, grid-template-rows: subgrid is an invalid declaration and gets thrown away. The card falls back to whatever your ordinary rules say, which is a card that lays itself out independently.
In other words, the fallback is exactly how every card on every website currently looks. Nobody has ever complained about it, because it’s the status quo, and the enhancement is that some people get the aligned version.
So there’s no @supports block to write and no second layout to maintain. Write the normal rules, add the subgrid on top, and the browsers that have it do the nice thing. I’ve had it in production since January and there is no fallback code anywhere.
What else is on that list
Six years between Grid arriving and the piece that finished it, and it’s not done yet in the browser most of my clients’ visitors use.
The thing I’d take from that isn’t impatience, it’s the noticing. I stopped complaining about this in about 2018 and had forgotten it was a workaround at all: the min-height on a card heading had become a thing I typed, rather than a thing I did because the platform couldn’t.
Which makes me wonder what else is on that list. The habits that started as compromises and got absorbed into how I think a card is built. I don’t have a way of auditing that, other than reading the specifications occasionally and being surprised 🥲