The "ragged footer" is one of the most persistent visual bugs in component-based web design. You have a row of pricing cards or feature teasers. The design mockup shows perfectly aligned titles, descriptions, and "Buy Now" buttons.
In implementation, reality hits. One title wraps to two lines. One description is slightly longer. Suddenly, your "Buy Now" buttons are scattered at different vertical positions, destroying the visual rhythm of the row.
For years, we hacked this. We used JavaScript to equalize heights (matchHeight.js). We used min-height with magic numbers. We used Flexbox with margin-top: auto on the footer to force it to the bottom of the card.
While margin-top: auto aligns the bottom of the footers, it does not align the start of the footers if the footers themselves vary in height, nor does it align the internal sections (like headers) across the row.
With CSS Grid Level 2 (Subgrid), we can solve this layout orchestration problem natively, mathematically, and without a single line of JavaScript.
The Root Cause: DOM Encapsulation
The fundamental issue is DOM encapsulation regarding layout context.
In a standard Flexbox or Grid layout, the parent container (.grid) controls the size and position of the cards (.card). However, the parent has no visibility into the internals of those cards.
Once the browser enters the .card context, layout calculations are isolated. The H3 in Card A has no relationship to the H3 in Card B. They are in different layout contexts. Consequently, Card A creates a row height based solely on its own content, oblivious to the fact that Card B has a taller header.
To achieve cross-card alignment, we must pierce this encapsulation. We need the internal tracks of the child to participate in the grid definition of the parent.
The Fix: CSS Subgrid
The solution requires inverting the layout logic. Instead of the card defining its own internal rows, the card defers its row definitions to the parent grid.
We will create a layout where every "visual" card actually spans multiple tracks (rows) in the main grid.
1. The HTML Structure
We need a standard semantic structure. No wrapper divs are required for the layout logic, just the content areas we want to align.
<section class="pricing-grid">
<!-- Card 1 -->
<article class="card">
<h2 class="card-title">Basic Tier</h2>
<div class="card-content">
<p>Essential features for small teams.</p>
</div>
<div class="card-footer">
<span class="price">$10/mo</span>
<button>Sign Up</button>
</div>
</article>
<!-- Card 2: Taller content -->
<article class="card">
<h2 class="card-title">Pro Tier (Best Value)</h2>
<div class="card-content">
<p>Advanced analytics, priority support, and unlimited projects for growing businesses.</p>
</div>
<div class="card-footer">
<span class="price">$29/mo</span>
<button>Sign Up</button>
</div>
</article>
<!-- Additional cards... -->
</section>
2. The CSS Implementation
Here is the modern, rigorous CSS solution.
:root {
--card-gap: 1.5rem;
--card-internal-gap: 1rem;
}
.pricing-grid {
display: grid;
/* Create responsive columns */
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
/*
CRITICAL:
We are not defining fixed rows. We let the content dictate the height.
However, layout is handled row-by-row across the entire grid.
*/
grid-auto-rows: auto;
/* This gap applies between columns AND between the 'subgrid' rows */
gap: var(--card-internal-gap) var(--card-gap);
/* Optional: Center the grid in viewport */
max-width: 1200px;
margin: 0 auto;
}
.card {
/*
1. SPANNING:
The card has 3 children (Title, Content, Footer).
We force the card to occupy 3 tracks in the parent grid.
*/
grid-row: span 3;
/*
2. SUBGRID:
We discard the card's internal row definitions.
The card's children now align directly to the parent's tracks.
*/
display: grid;
grid-template-rows: subgrid;
/* Visual styling for the card container */
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 1.5rem;
/*
NOTE: Margins/Padding on the card container don't affect
the alignment of the subgrid tracks, but visual boundaries do.
*/
}
/*
Reset margins on children to ensure the Grid Gap
is the only source of spacing logic.
*/
.card > * {
margin: 0;
}
Why This Works
The span 3 Logic
By applying grid-row: span 3 to the .card, we tell the .pricing-grid that each card item occupies three vertical slots.
- Row 1: Reserved for
.card-title - Row 2: Reserved for
.card-content - Row 3: Reserved for
.card-footer
The Subgrid "Teleportation"
When we apply grid-template-rows: subgrid to the .card, the card effectively stops being a layout boundary for its rows. The children of the card (h2, .content, .footer) are placed directly onto the tracks defined by the parent .pricing-grid.
Here is the resulting behavior for Row 1 (the Titles):
- The browser looks at the first row of the grid.
- It sees the
h2from Card 1 and theh2from Card 2. - It sizes that entire row track based on the tallest
h2in that row. - Because both
h2elements live in the same track, they are perfectly aligned.
The same logic applies to Row 2 (Content) and Row 3 (Footer).
If you have enough cards to wrap to a new visual line, the pattern repeats. Card 4, 5, and 6 will occupy Grid Rows 4, 5, and 6. They will align with each other, independent of the cards above them.
Handling Spacing Nuances
One technical constraint of subgrid is that the row-gap is inherited from the parent.
In the CSS above, I set gap: var(--card-internal-gap) var(--card-gap); on the parent.
- The column gap (
--card-gap) separates the cards horizontally. - The row gap (
--card-internal-gap) separates the header, body, and footer inside the card.
If you need the visual space between vertical rows of cards (e.g., between the bottom of Card 1 and the top of Card 4) to be larger than the internal space between a card's header and body, you have two options:
- Margin override: Add
margin-bottomto the.card. - Gap override (Preferred): Since the cards are spanning rows, the gap between cards vertically is actually just another grid track gap. Subgrid enforces uniform gaps inside the subgrid definition. The cleanest solution is usually adjusting the padding of the card container or accepting uniform vertical rhythm.
Conclusion
The subgrid value transforms CSS Grid from a container layout tool into a coordinate system that can penetrate deeply into component structures. By allowing the parent grid to govern the internal axis of its children, we solve the uneven footer alignment problem mathematically. We no longer force elements to match heights; we simply force them to share the same space.