CSS Subgrid in Production: Aligning Children Across Rows and Cols
CSS subgrid finally solves the alignment nightmare that nested grids created. Here's how to use it in real React components without reaching for JavaScript hacks.
Why Subgrid Exists (and What We Did Before It)
Honestly, nested CSS grids have been a lie we've all told ourselves for years. You define a beautiful 12-column grid on the parent, then create a card component inside it — and suddenly the card's internal content has no idea those parent columns exist. You're back to guessing pixel values and praying things line up.
Before subgrid, the workarounds were embarrassing. Some teams used a single flat grid and micromanaged every direct child with explicit grid-column spans. Others duplicated the column definition inside each nested component, meaning a design change required hunting down every .card, .panel, and .sidebar in the codebase. A few brave souls reached for JavaScript to measure DOM elements and apply transforms. None of these are solutions. They're band-aids.
CSS Subgrid (part of the CSS Grid Level 2 spec) shipped in Chrome 117, Firefox had it since version 71, and Safari added support in 16. As of late 2026, global browser support sits above 92%. You can use this in production without a polyfill.
The Core Syntax: grid-template-rows and grid-template-columns Set to subgrid
The API is disarmingly simple. On a child element that is already a grid item, you set display: grid and then set grid-template-columns: subgrid or grid-template-rows: subgrid (or both). That child now participates in the parent's grid tracks instead of creating its own.
Here's a concrete example. Say you have a product card grid where every card needs its image, title, description, and CTA button to line up across siblings — regardless of how much text each card has.
/* Parent grid */
.cards-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto;
gap: 24px;
}
/* Each card spans 4 implicit rows */
.card {
display: grid;
grid-row: span 4;
grid-template-rows: subgrid; /* inherit parent row tracks */
background: rgba(255,255,255,0.15);
border-radius: 12px;
padding: 16px;
}
/* Card children now align to parent row tracks */
.card__image { grid-row: 1; }
.card__title { grid-row: 2; }
.card__body { grid-row: 3; }
.card__cta { grid-row: 4; }That's it. No JavaScript. No ResizeObserver. No min-height hacks. The CTA buttons at the bottom of every card will align to the same row track regardless of title length or description word count.
Using Subgrid in React Components
The tricky part in React isn't the CSS — it's making sure your component hierarchy actually produces the right DOM structure. Subgrid only works when the subgrid item is a direct child of the parent grid. If you wrap things in extra <div> containers, you break the relationship.
Here's a pattern that works well with Empire UI card components. The parent CardGrid defines the row tracks, and each Card sets gridTemplateRows: 'subgrid'.
// CardGrid.tsx
export function CardGrid({ children }: { children: React.ReactNode }) {
return (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
gridAutoRows: 'auto',
gap: '24px',
}}
>
{children}
</div>
);
}
// Card.tsx
export function Card({ image, title, description, cta }: CardProps) {
return (
<article
style={{
display: 'grid',
gridRow: 'span 4',
gridTemplateRows: 'subgrid',
gap: '12px',
padding: '20px',
borderRadius: '10px',
background: 'rgba(255,255,255,0.08)',
backdropFilter: 'blur(12px)',
}}
>
<img src={image} alt={title} style={{ gridRow: 1, width: '100%', borderRadius: '6px' }} />
<h3 style={{ gridRow: 2, margin: 0 }}>{title}</h3>
<p style={{ gridRow: 3, margin: 0 }}>{description}</p>
<button style={{ gridRow: 4, alignSelf: 'end' }}>{cta}</button>
</article>
);
}Notice gridRow: 'span 4' on the article. This tells the parent grid that this item will occupy 4 row tracks. Then gridTemplateRows: 'subgrid' maps those 4 tracks down into the card's own grid. The children target specific rows with gridRow: 1, gridRow: 2, and so on.
Subgrid with Tailwind v4
Tailwind v4.0.2 added first-class subgrid utilities. You no longer need inline styles or custom CSS classes for the common cases. The relevant utilities are grid-rows-subgrid and grid-cols-subgrid.
The only gotcha: Tailwind's row-span-* utilities handle the span N part, but you still need to ensure the children target the right named rows. In practice this means combining Tailwind utilities with a small bit of custom CSS or CSS variables when your row count varies dynamically.
// Tailwind v4 approach
<div className="grid grid-cols-3 gap-6" style={{ gridAutoRows: 'auto' }}>
{cards.map(card => (
<article
key={card.id}
className="grid row-span-4 grid-rows-subgrid gap-3 p-5 rounded-xl bg-white/10 backdrop-blur-md"
>
<img className="row-start-1 w-full rounded" src={card.image} alt={card.title} />
<h3 className="row-start-2 m-0 text-lg font-semibold">{card.title}</h3>
<p className="row-start-3 m-0 text-sm text-zinc-400">{card.description}</p>
<a className="row-start-4 self-end btn-primary" href={card.href}>{card.cta}</a>
</article>
))}
</div>If you've been pairing Empire UI components with Tailwind — which is exactly what the library is built for — this approach fits naturally. For more on the Tailwind vs pure CSS tradeoffs in component libraries, check out our breakdown of Tailwind vs CSS Modules.
Aligning Across Both Axes: Columns and Rows Together
Most examples focus on row alignment, but you can subgrid on both axes simultaneously. This is where things get genuinely interesting for complex UI like dashboards or editorial layouts.
Imagine a 12-column magazine layout. Section components span multiple columns and their internal content — bylines, pull quotes, images — needs to align to both the parent column tracks and shared row tracks. Setting grid-template-columns: subgrid; grid-template-rows: subgrid; on the section gives you that two-dimensional alignment without any coordination logic between components.
The one limitation to know about: subgrid doesn't cascade through multiple levels. A grandchild of the original grid can't subgrid directly to the grandparent. You have to chain it — the child subgrids to the parent, then the grandchild subgrids to the child. It's a bit verbose but it works. This is worth noting if you're building deeply nested component trees similar to what you'd see in WebGL background effects where layered DOM structures are common.
There's also a gap inheritance subtlety. When you use subgrid, the child inherits the parent's gap by default. You can override it with your own gap value on the subgrid element if you want different internal spacing.
Real-World Pattern: Feature Comparison Tables
Feature comparison grids are the canonical subgrid use case. You have N columns (one per product tier) and M rows (one per feature). Without subgrid, getting every feature label and every checkbox icon to align perfectly across all tiers means either a single flat grid with hundreds of direct children, or JavaScript-measured heights.
With subgrid, each tier column becomes a subgrid item that spans all M rows. Its internal cells map to those parent rows. Add a new feature? Add a row to the parent. Every tier column updates automatically. This pattern scales well and it's exactly the kind of layout that benefits from the visual consistency that glassmorphism-style cards demand — you want those frosted panels to have perfectly aligned content, not a ragged mess.
Consider pairing this layout approach with a theme system. If you're building a dark/light mode toggle, the subgrid structure doesn't change — only CSS custom properties update. We covered how to wire that up cleanly in our theme toggle in React article. The separation of layout concerns from visual styling is one of the things subgrid gets right.
Browser Support, Fallbacks, and Progressive Enhancement
At 92%+ global support you're not going to break most users' experiences by shipping subgrid. But for that remaining 8% — mostly older Safari on iOS and older Chromium-based browsers on Android — you want a reasonable fallback.
The @supports query handles this cleanly. Define your fallback layout first (flexbox, or a simpler grid without subgrid), then wrap the subgrid-specific rules in @supports (grid-template-rows: subgrid). Browsers that don't support subgrid will get cards that still function — just without the pixel-perfect cross-card alignment.
/* Fallback: flex column inside each card */
.card {
display: flex;
flex-direction: column;
gap: 12px;
padding: 20px;
}
/* Enhancement: subgrid alignment when supported */
@supports (grid-template-rows: subgrid) {
.cards-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
.card {
display: grid;
grid-row: span 4;
grid-template-rows: subgrid;
}
}Progressive enhancement keeps the component functional everywhere while rewarding modern browsers with the better layout. Don't skip the fallback — it's five lines of CSS.
Common Mistakes and How to Debug Them
The most common mistake is forgetting grid-row: span N on the subgrid item. Without it, the parent doesn't allocate enough row tracks for the child to subgrid into, and everything collapses. If your subgrid looks broken, open DevTools and check how many rows the parent has allocated to that item.
Second mistake: adding extra wrapper divs inside the card without realizing they break the subgrid track mapping. If you have <article> → <div class='inner'> → content, the content is a child of the div, not a child of the article's subgrid. The fix is either to remove the wrapper or set display: contents on it — though display: contents has its own accessibility implications for elements that carry semantic meaning.
Can you use named grid lines with subgrid? Yes, and it's underused. Name your parent grid lines — [img-start] auto [img-end] auto [title-end] etc. — and subgrid children can reference those names directly. It makes the intent explicit and survives refactors better than numeric row indices. Firefox DevTools has the best subgrid visualization — their grid inspector shows named lines, track sizes, and the subgrid relationship clearly.
One more thing to watch: gap on a subgrid item applies between that item's children, not between the parent tracks. If your parent has gap: 24px and your subgrid card also has gap: 16px, children inside the card are spaced by 16px, not the parent's 24px. Usually you'll want to either remove the gap on the subgrid item or match it to the parent for visual consistency.
FAQ
Sort of. Named areas from the parent grid don't directly propagate to subgrid children. You can still use named lines — which are created implicitly by grid-template-areas — inside the subgrid. But you can't write grid-area: header in a grandchild and expect it to reference the parent's named area. Use explicit line names on the parent and reference those.
Yes, but with a caveat. When you use auto-fill or auto-fit with minmax(), the number of columns changes at runtime based on container width. Your subgrid item's grid-row: span N is fixed, so it works correctly. The column axis subgrid tracks will also respond to the auto-fill behavior as expected.
Firefox has better subgrid visualization as of 2026. In Firefox DevTools, open the Layout panel, enable the grid overlay, and you'll see both the parent grid tracks and the subgrid tracks labeled separately. Chrome DevTools shows grids but doesn't visually distinguish subgrid relationships as clearly. For complex layouts, spinning up Firefox specifically for debugging is worth it.
No. grid-rows-subgrid only sets grid-template-rows: subgrid. You still need row-span-{n} (e.g. row-span-4) separately on the same element to tell the parent how many tracks to allocate. Both classes are required on the subgrid item.
They still work, but they apply to the subgrid element's own children, not to the parent grid. The alignment properties don't inherit through subgrid — only the track sizes and positions do. You can set align-items on the parent and it won't affect direct children of a subgrid item inside it.
Negligible in practice. The browser has to do slightly more layout work to reconcile subgrid tracks with parent tracks, but the overhead is in microseconds, not milliseconds. I've seen no measurable performance difference in grids with up to 200 card items on a mid-range Android device. Don't let performance anxiety push you back to JavaScript layout calculations — those are dramatically slower.