The promise of CSS Grid Level 2 (Subgrid) is tantalizing for design systems: the ability to align nested components (like card headers, bodies, and footers) across sibling elements without hardcoding heights.
However, a specific implementation failure is pervasive. You apply grid-template-rows: subgrid to a child element, expecting its internals to snap perfectly to the parent's rhythm. Instead, the layout collapses. Content overlaps, or the child items shove themselves into a single track, completely ignoring the internal spacing you intended.
This collapse happens because subgrid does not create tracks; it exposes existing ones. If the parent item does not span enough tracks to accommodate its own children, the layout logic fails.
The Root Cause: The Span Dependency
In standard CSS Grid, a nested grid creates a new Independent Formatting Context. It calculates its own track sizes based on its own content.
When you declare grid-template-rows: subgrid, you are telling the browser: "Do not calculate track sizes for this element. Instead, look at the parent grid tracks I cover and pass those down to my children."
The error occurs when the element acting as the subgrid container resides in a single track of the parent (the default behavior), but has multiple children it needs to display.
- The Parent Grid places the
.cardinto a single cell (e.g., Row 1, Column 1). - The Card declares
grid-template-rows: subgrid. - The Browser looks at the
.card. It occupies 1 row in the parent. - The Result: The card's internal grid now has only 1 row available to distribute among its own children (Header, Body, Footer). All three children are forced into that single inherited track, causing overlap or layout collapse.
To fix this, the subgrid container must explicitly span the number of tracks required by its internal structure.
The Fix: Explicit Row Spanning
Let's look at a common scenario: a list of pricing cards. We want the headers, prices, and feature lists to align horizontally across the page, regardless of content length.
The Broken Implementation
This code fails because .card only occupies one row in .pricing-grid, so the subgrid has no tracks to offer its children.
/* ❌ DOES NOT WORK */
.pricing-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.card {
display: grid;
/* This creates a subgrid, but the card only exists in ONE row of the parent */
grid-template-rows: subgrid;
}
The Working Solution
We must restructure the parent to handle the row definition and force the card to span the specific number of tracks required by its children.
<section class="pricing-grid">
<!-- Card 1 -->
<article class="card">
<h2 class="card-title">Basic</h2>
<p class="card-price">$10/mo</p>
<ul class="card-features">
<li>1 User</li>
<li>5GB Storage</li>
</ul>
<a href="#" class="btn">Buy</a>
</article>
<!-- Card 2 -->
<article class="card">
<h2 class="card-title">Pro</h2>
<p class="card-price">$30/mo</p>
<ul class="card-features">
<li>5 Users</li>
<li>20GB Storage</li>
<li>Priority Support</li> <!-- Taller content -->
</ul>
<a href="#" class="btn">Buy</a>
</article>
<!-- Additional cards... -->
</section>
:root {
--gap-size: 1.5rem;
}
.pricing-grid {
display: grid;
/*
We define columns for the cards to sit side-by-side.
We do NOT define explicit row heights. We let the content dictate that.
*/
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
/*
CRITICAL:
We are not defining 'grid-template-rows' explicitly here because
we want infinite implicit rows to be generated as needed.
*/
gap: var(--gap-size);
}
.card {
display: grid;
/*
1. Activate Subgrid on the vertical axis.
The rows within this card now rely on the .pricing-grid's row definition.
*/
grid-template-rows: subgrid;
/*
2. THE FIX: The Magic Span.
We have 4 internal children (h2, p, ul, a).
We must tell the parent grid that this card occupies 4 rows.
*/
grid-row: span 4;
/* Visual styling */
background: #f4f4f5;
padding: var(--gap-size);
border-radius: 8px;
}
/*
Result:
- All .card-title elements across the entire row will share the height of the tallest title.
- All .card-features will expand to match the tallest feature list in the row.
- The 'Buy' buttons will always align at the bottom.
*/
Detailed Explanation
Why does grid-row: span 4 solve the alignment issue?
1. Shared Geometry
When .card is set to grid-row: span 4, the parent grid (.pricing-grid) reserves four distinct track lines for that specific item. Because all cards in the same visual "row" (in terms of columns) are auto-placed, the Grid algorithm ensures that "Row track 1" corresponds to the Header area for all cards in that line.
2. The Auto-Row Sizing
The parent grid uses grid-auto-rows (defaulting to auto).
- The browser looks at the first row track (occupied by all
.card-titleelements). - It calculates the height of the tallest title in that row.
- It sets the height of that track to fit that tallest content.
- Because every card is "subgridded" to this track, every card's title area expands to match.
3. Handling Gaps
A massive benefit of this approach is that gap is inherited. The row-gap defined in .pricing-grid is applied between the tracks. The .card does not need its own gap definition; the spacing between the H2, Price, Features, and Button is controlled entirely by the parent container's gap property.
Handling Variable Content
A common edge case arises when one card has fewer children than the span defines (e.g., a card missing a footer).
If you hardcode span 4 but a specific card only has 3 items, the last item will stretch to fill the remaining tracks, or you will end up with empty whitespace at the bottom of the card.
To handle dynamic children within a subgrid system, strictly map children to named lines or generic row indices:
.card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 4;
}
/* Force the last child to sit in the last row, regardless of DOM order */
.card > :last-child {
grid-row: -1 / -2;
}
Conclusion
subgrid is not a layout generator; it is a layout consumer. It cannot align your content if you don't allocate the space for it in the parent context.
When your nested grid collapses, stop debugging the child's internal CSS. Instead, check the parent. Ensure the subgrid container creates a span large enough to house its internals. The parent grid dictates the geometry; the child simply inhabits it.