An ongoing series on techniques that ship with the browser already - no library, no build step required.
Put three cards in a row and give them real content instead of placeholder text, and they stop lining up. One card’s copy runs a line longer than its neighbors, its footer drops a little lower, and the row that looked tidy in the mockup looks slightly off once actual words are in it. The usual fixes are a fixed height that breaks the moment the copy changes again, or reaching for JavaScript to measure each card and force them level.
Subgrid does it properly. Look at the middle card here - its text runs noticeably longer than the other two - and check where all three footers land:
Subgrid lines cards up to a grid their parent controls, not one each card invents for itself.
Field noteThis card's body just happens to run a couple of lines longer than its neighbors. Real copy rarely arrives in matching lengths, and a card grid has to survive that without anyone touching a height value by hand.
Field noteShort copy, same footer position as its longer neighbor.
Field noteHTML
<div class="sg-grid">
<article class="sg-card">
<span class="sg-eyebrow">Layout</span>
<p class="sg-body">Subgrid lines cards up to a grid their parent controls, not one each card invents for itself.</p>
<span class="sg-foot">Field note <span aria-hidden="true">→</span></span>
</article>
<article class="sg-card">
<span class="sg-eyebrow">Typography</span>
<p class="sg-body">This card's body just happens to run a couple of lines longer than its neighbors. Real copy rarely arrives in matching lengths, and a card grid has to survive that without anyone touching a height value by hand.</p>
<span class="sg-foot">Field note <span aria-hidden="true">→</span></span>
</article>
<article class="sg-card">
<span class="sg-eyebrow">Motion</span>
<p class="sg-body">Short copy, same footer position as its longer neighbor.</p>
<span class="sg-foot">Field note <span aria-hidden="true">→</span></span>
</article>
</div>CSS
.sg-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto 1fr auto;
gap: 1px;
background: var(--color-gray-200);
}
@media (max-width: 640px) {
.sg-grid {
grid-template-columns: 1fr;
}
}
.sg-card {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid;
gap: 0.6rem;
background: var(--color-white);
padding: 1.25rem;
}
.sg-eyebrow {
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 0.7rem;
font-weight: 700;
color: var(--accent);
}
.sg-body {
margin: 0;
font-size: 0.88rem;
line-height: 1.55;
color: var(--color-gray-600);
}
.sg-foot {
align-self: end;
font-size: 0.8rem;
font-weight: 600;
color: var(--color-gray-900);
}Every “Field note” sits on the same line, because the row it’s in was never sized by its own card - it was sized by the tallest one.
The whole idea
The parent grid declares three row bands - one for the eyebrow, one for the body, one for the footer - and each card is told to borrow them instead of inventing its own:
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto 1fr auto; /* eyebrow / body / footer */
gap: 1rem;
}
.card {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid;
}
grid-row: span 3 puts each card across all three of the parent’s row tracks. grid-template-rows: subgrid is the part that matters: instead of the card sizing its own three rows from its own content, it hands that decision back up to the parent. The parent then sizes each band by looking at the tallest content in that band, across every card at once - the eyebrow row becomes exactly as tall as the tallest eyebrow, the footer row exactly as tall as the tallest footer, and the middle row absorbs whatever’s left. A footer’s position stops being a property of its own card and becomes a property of the row it belongs to.
Why this beats “just make the cards the same height”
The instinctive flexbox fix - align-items: stretch - only equalizes the outer box. It makes all three cards the same overall height, but it does nothing to the space inside them. A card with less text still gets stretched to match its neighbors, but that extra space just piles up somewhere inside the card - usually between the body and the footer - so the footer itself can still land at a different height than a taller neighbor’s. Stretch matches outlines. Subgrid matches the actual rows inside them, which is the alignment you were after in the first place.
Where it stops working
Subgrid only makes sense between an element and a grid it’s genuinely a child of - you can’t reach two levels up, and you can’t link two unrelated grids together. It also arrived later than the rest of CSS grid, so if you’re supporting something unusually old it’s worth a quick check. Neither is a real obstacle today; it’s safe to build on directly - Firefox has had it the longest of any engine, in fact:
Browser support · subgrid
Support data from MDN’s browser-compat-data project (v8.0.4), sourced from Mozilla. Last checked 2026-07-03.
It’s the same category of fix as container queries: a real layout primitive replacing a pile of one-off patches, so a genuinely common problem - “line these up” - stops needing a workaround at all.



