Building a Responsive Dashboard Where Cards Align with Subgrid
Problem statement
You are building an analytics dashboard: a responsive grid of metric cards, each with a title, a chart or value area, and a footer of secondary stats. The grid auto-fits as many columns as fit, so cards land in rows of two, three, or four depending on viewport width. The problem is alignment. One card's title wraps to two lines, another's footer has three stats instead of one, and the result is a ragged row where nothing lines up. You want every card's title row, body row, and footer row to share a baseline across the entire row, with no fixed heights and no JavaScript measuring elements. This page solves exactly that with subgrid, then layers @container on top so each card's interior adapts to its own width. It builds directly on the techniques in the CSS Grid and Subgrid Layouts guide under Mastering Container Queries & Responsive Layouts.
Approach rationale
The pre-subgrid solutions were all compromises. Fixed min-height on each section guesses at content length and breaks the moment a label is longer than expected. A shared flex row forces every card into one row and abandons the auto-fit wrapping you want. JavaScript that measures the tallest card and sets the rest to match works, but it runs on every resize, fights the browser's own layout pass, and adds a layout-thrash hazard plus a hydration cost.
subgrid removes the guesswork entirely. The dashboard grid owns the row tracks; each card spans those rows and inherits their sizes, so alignment is computed once by the engine during normal layout. There is no measurement step and no reflow loop. The accessibility tradeoff is favorable too: because alignment is achieved without reordering or absolute positioning, DOM order stays equal to reading order, so keyboard and screen-reader users traverse the dashboard exactly as it appears. The only cost is a @supports gate for older browser versions, which is cheap.
Complete working implementation
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Subgrid dashboard</title>
<style>
:root { color-scheme: light dark; }
body {
margin: 0;
font: 16px/1.5 system-ui, sans-serif;
background: #f4f6fb;
color: #1c2430;
}
.dashboard {
display: grid;
/* Auto-fitting columns: no breakpoints needed for the grid itself. */
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
/* The shared row tracks every card will align onto:
title (auto), body (flexible), footer (auto). */
grid-auto-rows: auto 1fr auto;
gap: 1.25rem;
padding: 1.5rem;
max-width: 76rem;
margin-inline: auto;
}
.card {
display: grid;
/* Span the three implicit rows the parent created, and adopt them. */
grid-row: span 3;
grid-template-rows: subgrid;
row-gap: 0.75rem;
background: Canvas;
border: 1px solid #d4dbe8;
border-radius: 0.9rem;
padding: 1.1rem 1.25rem;
box-shadow: 0 1px 2px rgb(20 36 48 / 0.06);
/* Query the card's own width, not the viewport. */
container-type: inline-size;
container-name: card;
}
.card__title {
margin: 0;
font-size: 0.95rem;
font-weight: 600;
color: #5a6472;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.card__value {
margin: 0;
font-size: 2.4rem;
font-weight: 700;
line-height: 1.1;
}
.card__delta { font-size: 0.9rem; color: #1b8a5a; }
.card__delta--down { color: #c23b3b; }
.card__footer {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1rem;
margin: 0;
padding-top: 0.5rem;
border-top: 1px solid #e6eaf2;
font-size: 0.85rem;
color: #5a6472;
list-style: none;
}
/* Card-level responsiveness: when a single card is wide,
place the value beside its delta instead of stacking. */
@container card (min-width: 18rem) {
.card__body {
display: grid;
grid-template-columns: 1fr auto;
align-items: baseline;
column-gap: 0.75rem;
}
}
/* Older browsers: keep cards usable without subgrid alignment. */
@supports not (grid-template-rows: subgrid) {
.card { grid-row: auto; grid-template-rows: auto 1fr auto; }
}
</style>
</head>
<body>
<main class="dashboard">
<section class="card">
<h2 class="card__title">Monthly Recurring Revenue</h2>
<div class="card__body">
<p class="card__value">$48.2k</p>
<span class="card__delta">+12.4%</span>
</div>
<ul class="card__footer">
<li>New $6.1k</li>
<li>Churn $1.3k</li>
</ul>
</section>
<section class="card">
<h2 class="card__title">Active Users</h2>
<div class="card__body">
<p class="card__value">12,940</p>
<span class="card__delta card__delta--down">-2.1%</span>
</div>
<ul class="card__footer">
<li>DAU 4.2k</li>
</ul>
</section>
<section class="card">
<h2 class="card__title">Average Response Time Across All Regions</h2>
<div class="card__body">
<p class="card__value">214ms</p>
<span class="card__delta">+8ms</span>
</div>
<ul class="card__footer">
<li>p50 180ms</li>
<li>p95 410ms</li>
<li>p99 920ms</li>
</ul>
</section>
</main>
</body>
</html>
Notice that the three cards carry wildly different content: a two-line title on the third card, a single footer stat on the second, three on the third. Yet the value rows align, the footer top-borders align, and the cards are the same height per row. That alignment is produced entirely by the parent's grid-auto-rows plus each card's grid-template-rows: subgrid.
Key technique callout
The whole effect hinges on two declarations working as a pair. On the parent, grid-auto-rows: auto 1fr auto defines the three row tracks that the implicit rows will cycle through. On each card, grid-row: span 3 claims three of those rows and grid-template-rows: subgrid makes the card adopt their exact sizes instead of computing its own. Because all cards in a row span the same three parent rows, and those rows size to fit the tallest content in each band, every card's bands align. Drop the span 3 and a card inherits only one line pair, collapsing back to a single shared track; drop subgrid and each card sizes its rows independently and alignment vanishes. Both halves are required.
Variation or extension: reduced-motion aware card entrance
Dashboards often animate cards in on load. Keep that motion optional. The animation below fades and lifts each card, but collapses to an instant fade for anyone who has requested reduced motion, the same discipline taught in the Keyframe Animation Patterns guide where animating layout-affecting changes is covered.
@media (prefers-reduced-motion: no-preference) {
.card {
animation: card-in 360ms ease-out both;
}
}
@keyframes card-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: none; }
}
/* Reduced-motion users still get the content, just without movement. */
@media (prefers-reduced-motion: reduce) {
.card { animation: none; }
}
Browser support note
Subgrid is supported in Firefox 71+, Safari 16+, and Chrome/Edge 117+, which covers every current evergreen browser as of 2026, and the @container query layer is baseline since 2023. The only feature needing a fallback is subgrid in older versions, which the @supports not (grid-template-rows: subgrid) block above handles by restoring per-card row tracks; cards stay fully usable, they just lose cross-row baseline alignment.
FAQ
Why do my dashboard cards refuse to align across a row without subgrid? Each card runs its own independent grid, so its internal rows are sized only by its own content. Subgrid makes every card share the parent grid's row lines, so the title, body, and footer rows line up across all cards.
How many parent rows should a card span when using subgrid?
Span exactly as many rows as the shared internal sections you want aligned. For a title, body, and footer card that is three rows, written as grid-row: span 3 with grid-template-rows: subgrid.
Can I combine subgrid alignment with container queries on the same card? Yes. Subgrid governs how the card aligns with its siblings on the dashboard grid, while a container query restyles the card's interior based on the card's own width. They operate on different layers and do not conflict.
What happens to subgrid in browsers that do not support it?
Without a gate the card falls back to its own row tracks and loses cross-row alignment but stays usable. Wrap the subgrid rules in @supports (grid-template-rows: subgrid) and provide fixed inner rows as the baseline.
Related
- CSS Grid and Subgrid Layouts — the parent guide covering track sizing and subgrid in depth.
- Holy-Grail Layout with Grid — named areas for a full page skeleton.
- Building Responsive Cards with Container Queries — card internals that adapt to their own width.
- Keyframe Animation Patterns — animate card entrances accessibly.
Related articles
More pages in the same section.