Subgrid is the smallest CSS Grid feature with the biggest "oh, that's what was wrong" moment attached to it. Every developer who has built a row of cards has hit the problem it solves: three cards, three different title lengths, and three buttons that refuse to line up. For years the answers were fixed heights, JavaScript measuring loops, or quietly accepting the mess. Subgrid fixes it with one declaration — grid-template-rows: subgrid — and the idea behind it is simpler than the name suggests: a nested grid can borrow its parent's tracks instead of inventing its own.
The 8 demos below are all live and interactive, and most of them have a toggle that switches subgrid off so you can see the exact layout bug it was preventing. That contrast is the whole point — subgrid is hard to appreciate in isolation and obvious the moment you turn it off.
Every embed also has a Fork & Edit button that opens the exact snippet, already running, in My Code — FWD Tools' free in-browser editor. Change a track, delete a line, break it on purpose — that's a faster way to build real intuition than any explanation.
The one-sentence rule
.parent {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto 1fr auto;
}
.child {
grid-row: span 3; /* claim the rows you want to share */
display: grid;
grid-template-rows: subgrid; /* then adopt them instead of making new ones */
}
Normally, a nested grid is a sealed box: it divides its own width and height into fresh tracks, and its children have no idea the outer grid exists. subgrid removes that wall for one axis. The child still has to claim a range of the parent's tracks with grid-row or grid-column — subgrid doesn't guess — but from then on, its children are laid out on the parent's lines. That is the entire feature. Every demo below is one consequence of it.
1. Card Footers That Actually Line Up
Three cards, three different title and body lengths. Toggle subgrid off and watch the buttons scatter.
The rule:
.cards { grid-template-rows: auto 1fr auto; }
.card {
grid-row: span 3;
display: grid;
grid-template-rows: subgrid;
}
This is the canonical case, and worth being precise about why it works. The parent declares three row bands — title, body, footer — and every card spans all three. Because each card's rows are the parent's rows, the title band is sized once by the tallest title across all cards, and every card's title occupies that same band. The 1fr middle row absorbs the leftover height, which pushes every footer to the same line. With subgrid off, each card sizes its own three rows from its own content, so the buttons land wherever that card's text happens to end.
Best for: pricing tables, feature card rows, product grids — anywhere repeated cards need internal alignment, not just equal outer heights. Tip: equal card heights were never the hard part — align-items: stretch has done that since Flexbox. Subgrid solves the harder problem: equal positions for the content inside those cards.
2. A Form Where Every Input Lines Up
Each row is its own component with its own label. Subgrid makes them share the form's two columns.
The rule:
.form { grid-template-columns: max-content 1fr; }
.row {
grid-column: span 2;
display: grid;
grid-template-columns: subgrid;
}
The label column is max-content, which means "as wide as the widest label" — but the crucial detail is which labels get measured. With subgrid, all four rows share one column, so the widest label in the entire form sets the width and every input starts at the same x position. Turn it off and each row re-declares the same max-content 1fr template, which looks identical in the CSS but measures only that one row's label — so every input starts somewhere different. This is the classic case where you'd otherwise hardcode width: 140px on labels and hope nobody translates the page.
Best for: settings panels, checkout forms, any form where each field is a reusable row component rather than one flat list of elements. Tip: this pattern is what finally makes label-beside-input layouts safe for localisation — German or Finnish labels simply widen the shared column instead of breaking a hardcoded width.
3. A Nested Component on the Page Grid
The purple panel is a child of the layout, not a sibling of the columns. Subgrid lets its own children snap to the page's 12 columns anyway.
The rule:
.page { grid-template-columns: repeat(12, 1fr); }
.panel {
grid-column: 1 / 13;
display: grid;
grid-template-columns: subgrid;
}
.panelA { grid-column: 1 / 9; } /* real page columns, not panel columns */
.panelB { grid-column: 9 / 13; }
Design systems talk about "the 12-column grid" as if it applies to the whole page, but before subgrid it only ever applied to direct children of the grid container. Any component you nested — a section, a card, a wrapper div — started a brand-new coordinate system, and grid-column: 9 / 13 inside it meant something completely different from the same declaration outside it. Subgrid makes the column lines genuinely global: the ruler at the top of this demo is a sibling of the panel, yet the panel's children align to it exactly.
Best for: design-system layouts where components at any depth must respect one shared column grid. Tip: name your lines on the parent ([content-start], [full-end]) — named lines pass through subgrid too, which makes nested components far more readable than counting numbers.
4. Stat Tiles With Aligned Bands
Four KPI tiles: label, number, delta note. One label wraps to two lines and one delta is missing entirely — exactly the cases that break a flexbox row.
The rule:
.tiles { grid-template-rows: auto auto auto; }
.tile {
grid-row: span 3;
display: grid;
grid-template-rows: subgrid;
}
The two awkward tiles here are deliberate. One label wraps to two lines, which in a flexbox row pushes that tile's big number down out of alignment with the other three. One tile has no delta note at all, which in flexbox simply ends short. Subgrid handles both without special-casing: the label band is as tall as the tallest label, so the wrapped one doesn't push anything; and the missing delta leaves an empty cell in a band that still exists, so the tile keeps its shape.
Best for: dashboards and KPI rows, where the numbers scanning cleanly across a line is most of the value of the layout. Tip: reach for align-self inside the bands — align-self: end on the delta pins it to the bottom of its band regardless of how tall that band grows.
5. Full-Bleed From Inside a Nested Section
The figure is nested inside a <section>, two levels below the article grid. Subgrid is what lets it still reach the outer full-bleed column lines.
The rule:
.article {
grid-template-columns:
[full-start] 1fr [content-start] minmax(0, 24em) [content-end] 1fr [full-end];
}
.chapter {
grid-column: full-start / full-end;
display: grid;
grid-template-columns: subgrid; /* the named lines come with it */
}
.chapter > .bleed { grid-column: full-start / full-end; }
The "full-bleed inside a readable column" layout is well known, and it works fine for direct children of the article grid. The moment real content gets wrapped — in a <section>, a CMS block, a React component — the outer column lines become unreachable, and the usual workaround is the negative-margin hack (margin-inline: calc(50% - 50vw)) with all its scrollbar-width bugs. Subgrid passes the named lines down intact, so a figure three levels deep can still say grid-column: full-start / full-end and mean it.
Best for: article and documentation layouts where content is authored in nested blocks rather than one flat list. Tip: subgrid inherits the parent's line names as well as its track sizes, which is what makes this readable — without names you'd be counting anonymous line numbers through every level of nesting.
6. Table Alignment Without a Table
Each row is an independent component — its own card, its own border, its own hover state. Subgrid keeps their columns aligned as if they were one table.
The rule:
.list { grid-template-columns: 2fr 1fr max-content max-content; }
.row {
grid-column: 1 / -1;
display: grid;
grid-template-columns: subgrid;
}
This is the layout that genuinely had no good answer before. A real <table> aligns columns automatically but makes styling individual rows as cards painful. A grid of flat cells aligns perfectly but leaves no element to put a border or hover state on. Subgrid gives you both: each row is a real box you can style however you like, and its cells still sit on the list's shared columns. Toggle it off and you can see the difference clearly — every row re-declares the exact same 2fr 1fr max-content max-content template, but each one now measures its own content, so nothing agrees with the header.
Best for: data lists that need per-row interactivity — selectable rows, expandable rows, drag handles, per-row hover and focus states. Tip: if the data is genuinely tabular, still use a real <table> for the semantics and apply display: grid plus subgrid to the rows — you keep screen-reader table navigation and gain the styling freedom.
7. Subgrid Inherits the Parent's Gap
Drag the slider to change the parent's gap, then override the child's own gap and watch the two come apart.
The rule:
.parent { gap: 14px; }
.child {
grid-template-columns: subgrid;
/* no gap declared — inherits the parent's 14px */
}
.child.noGap { gap: 0; } /* override: tracks still align, spacing changes */
This one catches people out, so it's worth seeing directly. A subgrid uses its parent's gap by default — it has to, or the tracks would stop lining up. But you can override it, and the result is subtler than it first looks: the column positions still match the parent exactly, because those are fixed by the parent's tracks. What changes is only the space the child leaves between its own items. Dragging the slider with the override on makes this obvious — the outer spacing responds, the inner spacing doesn't.
Best for: understanding why your subgrid "nearly" lines up — a stray gap on the child is one of the two most common causes. Tip: the other common cause is padding. A subgrid's own padding shifts its contents inside the tracks without moving the tracks, so a padded subgrid row looks misaligned even though its grid is correct.
8. Subgrid vs display: contents
Both make a wrapper's children line up on the outer grid. Only one of them keeps the wrapper itself.
The rule:
.rowSub { display: grid; grid-template-columns: subgrid; } /* box survives */
.rowContents { display: contents; } /* box disappears */
display: contents was the pre-subgrid workaround, and for pure alignment it does work: the wrapper is removed from the box tree, so its children become direct grid items of the outer grid. The cost is the wrapper itself. In this demo both rows carry identical border, background and padding declarations — the subgrid row renders them, the display: contents row silently drops every one, because there is no longer a box to paint them on. It also historically removed the element from the accessibility tree in several browsers, which made it risky on semantic elements.
Best for: knowing which to reach for — display: contents when the wrapper is purely structural and you never need to style it; subgrid whenever the wrapper is a real visual component. Tip: if you're migrating old code, a display: contents row that you keep wanting to add a hover state or border to is exactly the signal to convert it to a subgrid.
Common pitfalls
- Forgetting to span the tracks first.
grid-template-rows: subgriddoes nothing useful on its own — the element must also claim a range withgrid-row: span 3(or explicit line numbers). Subgrid adopts the tracks you claimed, and claiming none means adopting none. - Expecting both axes at once.
subgridis set per axis. A child can be a subgrid for columns while defining its own rows normally — and often should be. Demo #2 subgrids only columns; demo #1 only rows. - A stray gap or padding on the child. Demo #7 shows both. The tracks still align, but the contents sit at the wrong offset inside them, which reads as "subgrid isn't working" when the grid is actually fine.
- Assuming it fixes unequal card heights. That was never the problem —
align-items: stretchalready did that. Subgrid aligns the content inside the cards, which is the part flexbox genuinely cannot do. - Reaching for
display: contentsout of habit. Demo #8 shows the cost: identical border and background declarations render on the subgrid row and vanish on thecontentsone.
Frequently asked questions
Can I use subgrid in production yet?
Yes — subgrid is supported in all current major browsers (Firefox since 71, Safari since 16, Chrome and Edge since 117). It also degrades unusually gracefully: a browser that doesn't understand grid-template-rows: subgrid ignores that one declaration and the nested element stays a normal grid, so you get the unaligned-but-functional version rather than a broken page. That makes it a reasonable progressive enhancement even where you can't guarantee support.
What's the difference between subgrid and just using one big flat grid?
A flat grid aligns everything perfectly but leaves you with no wrapper elements — no per-card border, no per-row hover state, no component boundaries that match your markup. Subgrid keeps the wrappers (and therefore the components, the semantics and the styling hooks) while still sharing one set of tracks. Demo #6 is the clearest illustration of that trade-off.
Does subgrid work on both rows and columns at the same time?
Yes, but you declare each axis separately: grid-template-columns: subgrid; grid-template-rows: subgrid;. You can also mix — subgrid one axis and define your own tracks on the other, which is often what you actually want. A card row usually subgrids rows only; a form row usually subgrids columns only.
Why do my subgrid items still look misaligned?
Almost always one of three things: the child didn't span any parent tracks (so there was nothing to adopt), the child declares its own gap, or the child has padding that offsets its contents inside otherwise-correct tracks. Demo #7 isolates the gap and padding cases specifically — they're the ones that look most like a broken grid when the grid is fine.
Can subgrid be nested more than one level deep?
Yes. A subgrid can itself contain another subgrid, and the tracks pass all the way down as long as each level spans a range and declares subgrid for that axis. Named lines pass down through every level too, which is what keeps deeply nested layouts (demo #5) readable instead of turning into line-number arithmetic.
Every demo above ships its full HTML, CSS and JS in the HTML / CSS / JS tabs in its own top bar. The fastest way to internalise this is to click Fork & Edit on demo #1, delete the grid-row: span 3 line but leave grid-template-rows: subgrid in place, and watch the alignment collapse — a subgrid that spans no parent tracks has nothing to adopt. That single experiment explains more than any diagram. Once you've got a version you like, save it to My Code and it's yours to keep, tweak further, or drop into a real project.
Want to go deeper than a single embed? The CSS Playground is FWD Tools' full in-browser CSS sandbox — a real editor with instant preview, built for exactly this kind of edit-and-see learning, without the eight-tab constraint of a blog post.
