← All writing
Craft · · 8 min

Subgrid

Subgrid is the piece that finishes Grid, and the half I keep turning over is how long I went without missing it.

CSS Cross-Browser Compatibility

A client sent back a screenshot of their own homepage with three red circles drawn on it, one around each of the buttons in a row of cards. Two of them were level with each other. The third one wasn’t, and they wanted to know whether that was deliberate.

It wasn’t. The heading on that card wrapped onto a third line where the other two stopped at two, so everything under it started a line lower, and the button at the bottom inherited the entire difference.

That isn’t a bug, or at least it isn’t one anybody wrote. Each card in that row is its own grid, with its own tracks and its own idea of where a row begins, and the rows inside the first card have no relationship of any kind to the rows inside the second. Grid hands you three cards of equal height, which is the part everybody notices. It hands you nothing at all about what happens inside them.

I’ve known that since before the feature that fixes it had a name. The week Grid launched I put one complaint at the end of an otherwise giddy post. The thing where a nested grid lines up with its parent’s tracks had been cut out of level 1 and pushed into a level 2 that didn’t exist yet. I said it was the omission I expected to miss most, because “the card’s internals line up with the page grid” is something designers ask for constantly.

I did miss it, for about a year. Then I stopped, and not because I stopped wanting it. I stopped noticing that I wanted it, the way you stop noticing a door that sticks and just start putting your shoulder into it.

In ascending order of shame

Four ways to fake this, and I’ve launched every one of them!

The cheapest is a min-height on the heading, sized for the longest title anybody on the client’s side seems likely to write. It holds until somebody writes a longer one, and in the meantime it parks a strip of dead space under every short one.

Then there’s clamping the heading to two lines, which aligns everything beautifully and does it by hiding words somebody was paid to write. That’s a content decision. I’ve made it on a client’s behalf more than once without mentioning that I made it.

Better than either is flexbox inside the card with margin-top: auto on the footer, which pushes the button to the bottom of its own card. The cards are already the same height, so the buttons genuinely do line up. This one I’d still defend in a code review. It does nothing whatsoever for the heading and the paragraph in the middle though, and the middle is most of the card.

And then there’s measuring the tallest heading in JavaScript and setting the rest to match. There were jQuery plugins for this and I used one for years. It runs on load, runs again on resize, argues with the web fonts arriving, and when it loses that argument you get to watch the row shuffle itself into place after the page has already drawn 🥲

Not my rows

grid-template-rows: subgrid tells a nested grid to stop making up rows of its own and use its parent’s instead.

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

/* image, heading, body, footer */
.card {
  grid-row: span 4;
  display: grid;
  grid-template-rows: subgrid;
}
CSS

Every card now drops its four parts into the same four rows of the outer grid, and the outer grid sizes each of those rows to the tallest thing sitting in it across all the cards. The headings line up because every heading is in row two, and row two is as tall as the tallest heading. The buttons line up because every button is in row four.

No heights, no clamping, no measuring, no script. I like best that it holds up when somebody pastes in a title twice as long as anything I designed for, because the row simply grows and everything underneath it is still aligned.

It does the same job on a form, which is where I’ve wanted it longer. Labels and inputs lining up all the way down a form even when every field is its own component, which otherwise wants either one flat lump of markup or a label column with a width that somebody guessed.

You have to call your shot

Subgrid on its own does nothing, which had me confused for a while.

The card has to actually occupy the rows it intends to subdivide, which is what grid-row: span 4 is doing up there. The card claims four rows of the parent, and then subgrid says its own children go into those four. Leave the span off and the card takes one row, subdivides one row, and nothing lines up with anything.

Which is the one real constraint on the feature. You have to know upfront how many parts the component has. If a card sometimes has an image and sometimes doesn’t, the span isn’t a constant anymore, and you’re either reserving the row whether or not it gets used or reaching for a relational selector to vary the number. Manageable, and a genuine wrinkle for CMS-driven content, where every part of every card is optional by definition.

There’s a joke buried in that fix which I only noticed writing this down. The tidy way to vary the span is :has(), which is in Chrome and Safari and not Firefox, while subgrid is in Firefox and Safari and not Chrome. So you can have one of them everywhere your visitors actually are, or you can have both in Safari.

Two smaller things worth knowing before you type it. Gaps come down from the parent unless the child sets its own, which is usually what you want and is occasionally a surprise. And the two axes are independent, so you can subgrid the rows without touching the columns, and rows are the one you’ll want nearly every time.

So can I use it?

Firefox has had subgrid since 71, which came out at the end of 2019. It had the feature to itself for the better part of three years, which is a strange sentence to type a few weeks after writing that everybody was waiting on Firefox for container queries. Safari 16 got it in the fall. Chrome is the one that’s left, the work has been going on in Chromium in the open for a while, subgrid is on the browsers’ shared Interop list for this year, and I don’t have a date for you.

Which would normally make this a check-back-in-a-year post, except that the way this thing degrades is the best I have run into.

A browser that doesn’t understand a value throws the whole declaration away. That’s just how CSS works, and here it means the card falls back to whatever your ordinary rules already say, which is a card that lays itself out on its own, which is exactly how every card on every website currently looks. Nobody has ever filed a bug about that, because it is the status quo. The enhancement is that some people get the tidy version.

So there’s no @supports block to write and no second layout to keep in step with the first. Write the normal rules, put the subgrid on top of them, and the browsers that have it do the nice thing. I put it on that client’s card grid a couple months ago and there is no fallback code anywhere in the file, because the fallback is the code that was already there. This is the flavor of progressive enhancement that survives a deadline, the one that’s free.

What else did I stop noticing?

Six years between Grid arriving and the piece that finishes it. It still isn’t finished in the browser most of my clients’ visitors are using.

The wait isn’t the part I keep turning over though. When I took the min-height off that client’s card headings I went looking for the others in the same stylesheet. I couldn’t tell you which of them were me compensating for a missing feature and which were somebody’s actual design intent, because they are the same declaration and a number in a stylesheet doesn’t record why it’s there. I stopped complaining about this one around 2018. Somewhere in the five years after that it quit being a workaround and became a thing I type on a card heading, the way I type box-sizing.

I don’t have a method for finding the rest of them. Reading the changelogs helps a bit, but a changelog tells you what arrived and not what you’d quietly given up on asking for, and the whole trouble is that the giving up doesn’t feel like anything at the time. There is presumably a way of auditing your own habits for this, but right now mine is being surprised every couple of years by a client with a red circle.

Read similar posts
9 min

Put the box under the button

The popover attribute gave me the top layer last spring and left me doing all the math, and the CSS that finishes the job has landed in one browser and deletes a positioning library, 2 scroll listeners and a resize observer.

9 min

Styling on what's inside, with :has()

A card came out with its heading jammed against the top border, because the class that says a card has no image gets added by a template, and the template rendering that particular row had never been told to add it.