The Hook: The "Ragged Row" Problem
For years, creating a grid of "cards"—layout components containing a header, varying lengths of content, and a footer/action area—has plagued UI designers with a subtle but infuriating alignment issue.
While Flexbox and standard CSS Grid made equal-height cards easy (height: 100% or default stretch behavior), they failed to align the internals of those cards. If Card A has a two-line title and Card B has a one-line title, the content bodies start at different vertical positions. Consequently, the "Read More" buttons at the bottom never sit on the same visual scan line.
Historically, we solved this with fragility: fixing heights (which breaks overflow), display: table hacks, or JavaScript loops that measure DOM elements and forcibly set inline styles. These methods are computationally expensive, prone to "Flash of Unstyled Content" (FOUC), and hostile to maintenance.
With CSS Grid Level 2, specifically subgrid, we can now solve this natively.
The Why: Layout Encapsulation
To fix the problem, we must understand why it exists. The root cause is layout encapsulation.
In a standard CSS Grid or Flexbox layout, the parent container controls the size and position of the direct children (the cards). However, the parent has no awareness of the grandchildren (the card headers, bodies, or footers).
Inside the card, a new formatting context is established. The card distributes space based solely on its own content. Card A calculates its layout in isolation from Card B. Because there is no shared context between the internals of siblings, visual alignment across the row is impossible without manual intervention.
CSS Subgrid breaks this encapsulation. It allows a grid item (the card) to defer its internal track sizing to the parent grid. This means the grandparent container determines the height of the card's header, ensuring it accommodates the tallest header in that specific row.
The Fix: Implementation
We will build a responsive card grid where:
- Headers always align across the row.
- Bodies expand to fill available space.
- Footers (actions) always stick to the bottom, aligned perfectly with neighbors.
1. The HTML Structure
We need semantic markup. Each card contains three distinct sections.
<section class="card-grid">
<!-- Card 1: Short content -->
<article class="card">
<h2 class="card-head">Standard Plan</h2>
<div class="card-body">
<p>Perfect for starters.</p>
</div>
<footer class="card-foot">
<button>Buy Now</button>
</footer>
</article>
<!-- Card 2: Long content forcing expansion -->
<article class="card">
<h2 class="card-head">Enterprise Plan with a significantly longer title</h2>
<div class="card-body">
<p>Includes analytics, prioritized support, and dedicated infrastructure.</p>
<p>Our most popular option for scaling teams.</p>
</div>
<footer class="card-foot">
<button>Contact Sales</button>
</footer>
</article>
<!-- Card 3: Mixed content -->
<article class="card">
<h2 class="card-head">Pro Plan</h2>
<div class="card-body">
<p>Everything in Standard, plus:</p>
<ul>
<li>Feature A</li>
<li>Feature B</li>
</ul>
</div>
<footer class="card-foot">
<button>Upgrade</button>
</footer>
</article>
</section>
2. The CSS
The solution relies on the parent grid defining the rows, and the child cards using grid-template-rows: subgrid.
/* 1. THE PARENT GRID */
.card-grid {
display: grid;
/* Responsive columns */
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
/* CRITICAL: Define the specific row rhythm.
We are not defining specific heights, but rather "auto" sizing.
However, we are relying on the implicit grid to handle the
repetition of this pattern for every card. */
}
/* 2. THE CARD (Subgrid Container) */
.card {
/* The card itself is a grid item, but also a grid container */
display: grid;
/* The Magic: Inherit the row tracks from the .card-grid parent */
grid-template-rows: subgrid;
/* The Logic: Each card spans 3 rows (Head, Body, Foot) */
grid-row: span 3;
/* Visuals */
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
}
/* 3. HANDLING ROW SIZING ON THE PARENT */
/*
Because the cards are subgrids, the sizing logic bubbles up.
We explicitly tell the parent grid's tracks how to behave.
Since the cards span 3 rows, standard auto-placement needs help
understanding the repeating pattern.
However, a cleaner modern approach avoids complex parent row definitions
by simply letting the content define the tracks naturally,
provided the card spans correctly.
*/
/*
Refined Approach:
We don't actually need to set grid-template-rows on the .card-grid
if we want pure content-based sizing. The subgrid items (grandchildren)
will dictate the track sizes of the implicit rows in the parent.
*/
/* 4. INTERNAL ALIGNMENT */
.card-head {
padding: 1.5rem;
background: #f8fafc;
align-self: start; /* Align content to top of the subgrid track */
}
.card-body {
padding: 1.5rem;
/* This creates the expansion effect in the middle track */
align-self: stretch;
}
.card-foot {
padding: 1.5rem;
border-top: 1px solid #e2e8f0;
align-self: end; /* Align content to bottom */
}
The Explanation
How Layout Bubbling Works
When you declare grid-template-rows: subgrid on the .card, you are effectively deleting the row definition of the card and replacing it with a reference to the parent.
Here is the chain of events:
- Span Declaration: We set
grid-row: span 3on the card. This tells the Layout Engine that every card occupies three vertical "tracks" in the main grid. - Aggregation: The browser looks at "Track 1" (the header row) across all cards currently sharing that geometric row.
- Sizing: It finds the tallest
<h2>among those cards. It sets the height of "Track 1" for the entire row to that maximum height. - Repeat: It does the same for the body (Track 2) and footer (Track 3).
Because the .card-body resides in a track that is sized by the tallest body content in the row, shorter bodies will exist inside a taller track. By default, subgrid passes down the parent's gap, ensuring spacing consistency without redefining margins.
Handling Responsiveness
This solution is intrinsically responsive.
When grid-template-columns wraps the cards onto a new visual line (e.g., on mobile), the browser creates new implicit row tracks for that new line. The subgrid logic applies to this new set of tracks. The cards on the second row calculate their alignment based only on their neighbors on the second row, independent of the first row.
Conclusion
CSS Subgrid transforms the relationship between parent and grandchild elements. By allowing cards to participate in the sizing logic of their container, we eliminate the need for JavaScript layout thrashing and brittle min-height estimates.
This approach provides a robust, pixel-perfect layout system that respects content dynamism and maintains architectural strictness. As of 2024, this feature is supported in all major evergreen browsers. Use it to delete your layout calculation scripts.