
CSS Subgrid: Aligning Nested Layouts
You've built a row of cards with CSS Grid. The cards themselves line up perfectly, but inside them, things fall apart: one card has a two-line title, so its description starts lower than its neighbours, and the "Read more" buttons sit at three different heights. You reach for min-height hacks, fixed title heights, or JavaScript that measures everything. None of it feels right.
This is exactly the problem subgrid was designed to solve. With grid-template-columns: subgrid or grid-template-rows: subgrid, a nested grid can adopt the tracks of its parent instead of defining its own. Items in different cards end up sharing the same rows, so they align automatically, no matter how much content each one holds.
In this guide, I'll walk you through how subgrid works, the mental model you need, and several real layouts where it removes a whole category of alignment bugs.
Why Nested Grids Don't Align on Their Own
To understand why subgrid matters, it helps to see what goes wrong without it.
When you set display: grid on an element, only its direct children become grid items. Grandchildren don't participate in that grid at all. If you want the contents of a card to be laid out on a grid too, you have to make the card its own grid:
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
.card {
display: grid;
grid-template-rows: auto auto 1fr auto;
}
That works inside each card, but each card's grid is completely independent. The auto row for the title in card one is sized by card one's title only. Card two has no idea that card one's title wrapped onto two lines. So the rows in different cards end up with different heights, and nothing lines up across the row.
Flexbox has the same limitation. Every flex container sizes its own contents in isolation. What we actually want is for the cards to share a single set of row tracks, and that's what subgrid gives us.
The Core Concept
A subgrid is a grid container that, instead of creating new tracks in one or both dimensions, inherits the tracks from its parent grid. You opt in per axis:
.child {
display: grid;
grid-template-rows: subgrid; /* use the parent's rows */
/* or */
grid-template-columns: subgrid; /* use the parent's columns */
}
A few rules make this work:
- The subgrid element must itself be a grid item of a parent grid. It has to span some tracks of that parent.
- The subgrid uses exactly the tracks it spans. If it spans four rows of the parent, it has four rows.
- Items placed inside the subgrid contribute to the sizing of the parent's tracks. That's the magic: a tall title in one card makes the shared title row taller for every card.
- Gaps are inherited by default, but you can override them on the subgrid with
gap,row-gap, orcolumn-gap. - Line names from the parent are available inside the subgrid, and you can add your own.
The key insight is that subgrid isn't a special layout mode. It's just a grid that borrows its track definitions. Everything else you know about Grid still applies.
Step-by-Step: Aligned Card Rows
Let's fix the card problem from the introduction. Here's the markup:
<ul class="cards">
<li class="card">
<img src="/images/lighthouse.jpg" alt="" class="card__media" />
<h3 class="card__title">Lighthouse audits in CI</h3>
<p class="card__body">Run performance checks on every pull request.</p>
<a href="/guides/lighthouse" class="card__link">Read guide</a>
</li>
<li class="card">
<img src="/images/fonts.jpg" alt="" class="card__media" />
<h3 class="card__title">
Loading web fonts without layout shift or invisible text
</h3>
<p class="card__body">
A practical look at font-display, preloading, and size-adjust.
</p>
<a href="/guides/fonts" class="card__link">Read guide</a>
</li>
<li class="card">
<img src="/images/images.jpg" alt="" class="card__media" />
<h3 class="card__title">Responsive images</h3>
<p class="card__body">
srcset, sizes, and picture explained with real examples, plus the common
mistakes that make browsers download the wrong file.
</p>
<a href="/guides/images" class="card__link">Read guide</a>
</li>
</ul>
Each card has four parts: media, title, body, and link. So each card needs to span four rows of the parent grid.
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
gap: 1.5rem;
padding: 0;
list-style: none;
}
.card {
display: grid;
grid-row: span 4;
grid-template-rows: subgrid;
row-gap: 0.75rem;
padding: 1rem;
border: 1px solid #e2e8f0;
border-radius: 0.75rem;
background: #fff;
}
.card__media {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
border-radius: 0.5rem;
}
.card__title {
margin: 0;
font-size: 1.125rem;
line-height: 1.3;
}
.card__body {
margin: 0;
color: #475569;
}
.card__link {
align-self: end;
font-weight: 600;
}
Here's what's happening:
.cardsis the parent grid. It defines columns but no explicit rows, so rows are created implicitly asauto.- Each
.cardsaysgrid-row: span 4. That makes the card occupy four implicit rows of the parent. grid-template-rows: subgridtells the card to use those four parent rows as its own rows.- The four children of each card land in those four rows, in order.
Now the title row's height is determined by the tallest title in that visual row of cards. The same goes for the body row. Every "Read guide" link sits on the same baseline across the row. When the viewport shrinks and the layout wraps to fewer columns, each new visual row of cards gets its own set of four shared tracks.
Notice the row-gap: 0.75rem on .card. Without it, the card would inherit the parent's 1.5rem gap between its internal rows, which is usually too loose inside a card. Overriding the gap on the subgrid is completely fine and doesn't affect the parent.
A Note on the Parent's Row Gap
Because the parent's rows are now shared by the card internals, the parent's row-gap applies between the internal rows of each card too, unless you override it. But the space between two rows of cards is also a parent row gap. If you override the gap on the subgrid, the space between card rows still comes from the parent. That's usually what you want: tight spacing inside the card, generous spacing between rows of cards.
Subgrid in Both Directions: A Form Layout
Forms are another place where alignment breaks. You want every label in one column, every input in the next, and help text in a third, even when fields are wrapped in fieldset elements for accessibility.
<form class="settings">
<fieldset class="settings__group">
<legend>Profile</legend>
<label for="name">Display name</label>
<input id="name" type="text" />
<small>Shown on your public profile.</small>
<label for="email">Email address</label>
<input id="email" type="email" />
<small>We never share this.</small>
</fieldset>
<fieldset class="settings__group">
<legend>Notifications</legend>
<label for="digest">Weekly digest frequency</label>
<select id="digest">
<option>Weekly</option>
<option>Monthly</option>
</select>
<small>How often we email a summary.</small>
</fieldset>
</form>
Without subgrid, each fieldset would size its label column independently, so the inputs in "Profile" and "Notifications" wouldn't line up. With subgrid on the column axis, they share the form's columns:
.settings {
display: grid;
grid-template-columns: max-content minmax(12rem, 1fr) minmax(0, 16rem);
gap: 0.75rem 1.25rem;
max-width: 60rem;
}
.settings__group {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
align-items: center;
margin: 0;
padding: 1rem;
border: 1px solid #cbd5e1;
border-radius: 0.5rem;
}
.settings__group legend {
grid-column: 1 / -1;
font-weight: 700;
padding-inline: 0.25rem;
}
.settings__group small {
color: #64748b;
}
The label column is sized with max-content, and because the labels in both fieldsets contribute to that one shared column, the column is as wide as the longest label anywhere in the form. Every input starts at the same horizontal position.
Two details are worth noting. First, grid-column: 1 / -1 makes each fieldset span all three parent columns, so the subgrid gets all three. Second, the padding on the fieldset is subtracted from the subgrid's edge tracks. Subgrid accounts for this automatically, which is why the inputs still align even though the fieldset has padding and a border.
Historically fieldset had some quirks as a grid container, but modern browsers handle display: grid on it correctly.
Named Lines and Subgrid
Line names flow down from the parent into the subgrid. This is useful for full-bleed article layouts, where you want most content in a readable centre column but some elements to break out.
.article {
display: grid;
grid-template-columns:
[full-start] minmax(1rem, 1fr)
[content-start] minmax(0, 42rem)
[content-end] minmax(1rem, 1fr)
[full-end];
}
.article > * {
grid-column: content;
}
.article > .figure-row {
grid-column: full;
display: grid;
grid-template-columns: subgrid;
}
.figure-row img {
grid-column: full;
width: 100%;
}
.figure-row figcaption {
grid-column: content;
}
Here .figure-row spans the full width and becomes a subgrid on columns. Inside it, the image can span full while the caption snaps back to content, aligned exactly with the paragraph text above and below. Using grid-column: content works because naming lines content-start and content-end creates an implicit named area called content.
You can also add line names on the subgrid itself:
.figure-row {
grid-template-columns: subgrid [edge] [text] [text-end] [edge-end];
}
These names are appended to the inherited ones, so you can use whichever vocabulary makes sense locally.
Subgrid on One Axis, Normal Tracks on the Other
You don't have to subgrid both axes. A common pattern is subgrid on rows (for alignment) with ordinary columns inside:
.pricing {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
.plan {
display: grid;
grid-row: span 4;
grid-template-rows: subgrid;
grid-template-columns: 1fr auto;
}
.plan__name {
grid-row: 1;
grid-column: 1;
}
.plan__badge {
grid-row: 1;
grid-column: 2;
align-self: start;
}
.plan__price {
grid-column: 1 / -1;
}
.plan__features {
grid-column: 1 / -1;
}
.plan__cta {
grid-column: 1 / -1;
align-self: end;
}
Each plan card has its own two-column internal layout for the name and a "Popular" badge, but its rows are shared with the other plans, so prices, feature lists, and buttons line up across all three.
Pay attention to the span. Each plan has five children, but the name and badge share the first row, so the card only needs four rows: name and badge, price, features, and CTA. Placing the name and badge explicitly in grid-row: 1 guarantees they share it, and the remaining three items flow into rows two to four. Count the rows you actually place content into, not the number of children. Getting this count wrong is the single most common subgrid bug.
Common Pitfalls
Forgetting the Span
If a subgrid element only spans one track (the default), it only gets one track. All its children will pile into that single row or create implicit tracks that are not shared with the parent. Always set grid-row: span N or grid-column: span N to match the number of tracks you need.
Implicit Tracks Inside a Subgrid
A subgrid can't create implicit tracks in a subgridded axis. If you put five items into a subgrid that spans four rows, the extra item doesn't get a new shared row. It overlaps into the last available track instead. If content is variable, compute the span from the maximum number of sections, and leave unused tracks empty.
Expecting Grandchildren to Participate
Subgrid only passes tracks down one level at a time. If the card's title is wrapped in a div with the heading inside, that div is the grid item, not the heading. If you need deeper nesting, the intermediate element must also be a subgrid, or you can use display: contents on the wrapper, keeping in mind that display: contents removes the element's box and can affect semantics for some elements like buttons.
Padding Confusion
Padding and borders on the subgrid element are accounted for by adding space to the first and last tracks it spans. That's what you want for alignment, but it means the outer tracks of the parent can grow when a subgrid has large padding. If the parent's edge tracks look wider than expected, check the subgrid's padding.
Overusing It
Not every nested grid needs subgrid. If the nested content has no relationship with its siblings, an independent grid is simpler and cheaper. Reach for subgrid when elements in different containers need to align with each other.
Browser Support
Subgrid is supported in all current major browsers: Firefox has had it for years, and Safari and Chromium-based browsers added support in 2023. It's now part of the Baseline feature set, so for most audiences you can use it without a fallback.
If you do need to support older browsers, a feature query keeps things graceful:
.card {
display: grid;
grid-template-rows: auto auto 1fr auto; /* independent fallback */
}
@supports (grid-template-rows: subgrid) {
.card {
grid-row: span 4;
grid-template-rows: subgrid;
}
}
In unsupported browsers, each card still has a sensible internal layout; it just won't align across cards. That's a perfectly acceptable progressive enhancement.
Debugging Subgrids
Browser DevTools have good subgrid support. In Firefox and Chromium-based browsers, the grid overlay badge appears next to subgrid elements in the Elements panel, and toggling it shows the inherited tracks. Enable overlays on both the parent and one of the subgrids at the same time. If the lines don't coincide, the span or placement is off.
A quick trick while developing: temporarily add an outline to every child of a subgrid.
.card > * {
outline: 1px dashed rgb(244 114 182 / 0.6);
}
You'll see instantly whether each piece is landing in the row you expect.
Conclusion
Subgrid closes a gap that existed since CSS Grid first shipped: nested content could never align with content in sibling containers without hacks. By letting a grid item adopt its parent's tracks, subgrid makes card rows, forms, pricing tables, and full-bleed article layouts line up automatically, while keeping your markup semantic.
The mental model is short. Make the child a grid item that spans the tracks it needs, set subgrid on the axis you want to share, and let the parent's track sizing take care of the rest. Count your spans carefully, override the gap when it's too loose, and you'll rarely need a fixed height for alignment again.
Try it on the next card component you build. Once you've seen three buttons snap onto the same line without a single magic number, it's hard to go back.


