Building Layout Algorithms with subgrid and Masonry
Card headers and footers that refuse to line up across a grid, or a gallery with wildly uneven image heights, need two different layout algorithms. Here's which one solves which problem.
Paul Radford — Full-Stack Developer & Editor. Paul is a full-stack developer and editor of template.tips. Articles here are AI-drafted and reviewed by Paul, who knows the code well enough to catch what's wrong and cut the hype.

Subgrid and CSS Masonry solve different layout problems and should not be treated as interchangeable tools. Subgrid lets nested grid items align to an ancestor grid's tracks, which is ideal for card components that need shared column or row alignment. Masonry packs items into the shortest available column, which suits galleries and dashboards with variable-height content. Combining both gives you fine-grained control without JavaScript layout libraries.
Why does subgrid matter more than nested grids alone?
A plain nested grid creates its own independent track sizing. That's fine until you need a card's header, body, and footer to line up with the headers, bodies, and footers of sibling cards, especially when those cards live inside a parent grid with irregular content lengths. Without subgrid, you either duplicate track definitions by hand (fragile, breaks on content changes) or reach for JavaScript to measure and sync heights (expensive, causes layout thrashing on resize).
Subgrid removes that duplication. A child grid inherits the exact track sizing of its parent for whichever axis you specify, so alignment stays correct even when parent tracks change size.
1/* Parent grid defines the master rows */2.card-grid {3 display: grid;4 grid-template-columns: repeat(3, 1fr);5 grid-template-rows: auto auto auto; /* header, body, footer rows */6 gap: 1.5rem;7}89/* Each card spans one column but subgrids the row axis */10.card {11 display: grid;12 grid-row: span 3;13 grid-template-rows: subgrid; /* inherit parent's row tracks */14 gap: 0.5rem;15}1617.card > .card-header { grid-row: 1; }18.card > .card-body { grid-row: 2; }19.card > .card-footer { grid-row: 3; }The payoff: if one card's header wraps to two lines, every header row across the grid grows to match, and every footer still lines up at the bottom. You get that behavior without measuring anything in JavaScript.
The catch: subgrid only inherits tracks on the axis you declare. If you write grid-template-columns: subgrid but leave rows as auto, the row axis behaves like a normal grid, not a subgrid. Developers frequently assume subgrid is all-or-nothing and get surprised when only half their alignment problem is solved.
Where subgrid breaks down
Subgrid requires an unbroken grid ancestry chain for the axis in question. If there's a non-grid wrapper (a flex container, or a div with no display: grid) between the subgridding element and its intended ancestor, the inheritance chain breaks and the subgrid falls back to auto-sized tracks. This is the single most common bug reported when people first adopt subgrid: they wrap a card in a <section> for semantic reasons without realizing that element now needs display: grid and grid-template-columns: subgrid (or grid-template-rows: subgrid) to pass the tracks through.
Browser support is solid as of 2026: Firefox has supported subgrid since version 71 (released in late 2019), Safari added it in Safari 16 (September 2022), and Chrome shipped it in Chrome 117 (September 2023). If you need to support anything before those versions, you cannot rely on subgrid and should fall back to a JavaScript-measured layout or accept independent track sizing per card. Check caniuse for the current matrix before committing to it in a project with strict legacy support requirements.
How do you build a masonry layout without JavaScript?
The CSS Working Group's answer is grid-template-rows: masonry, part of the CSS Grid Level 3 draft. As of early 2026, this is implemented behind a flag in Firefox Nightly and has no shipping support in Chrome or Safari. Practically, this means production sites cannot rely on native masonry yet, and you have three real options: fake it with grid using grid-auto-flow: dense combined with manually sized rows, use CSS columns (multi-column layout), or use a small JavaScript library.
The columns approach is the most broadly compatible today and requires no JavaScript:
1/* Column-based masonry using multicol, works in every current browser */2.masonry {3 columns: 3 250px; /* 3 columns, each at least 250px */4 column-gap: 1.5rem;5}67.masonry-item {8 break-inside: avoid; /* keep each item intact across columns */9 margin-bottom: 1.5rem;10 background: #fff;11 border-radius: 8px;12 padding: 1rem;13}This works everywhere columns is supported, which is essentially all browsers released since 2011, per MDN's compatibility data. The trade-off: items flow top-to-bottom within each column first, then wrap to the next column, so visual order does not match DOM order left-to-right. For a photo gallery that's invisible to users. For a news feed where reading order matters, it's a real accessibility problem, because screen reader users will hear items in an order that doesn't match what's visually adjacent.
Faking masonry with grid when column order matters
If you need left-to-right, top-to-bottom reading order preserved while still packing items efficiently, grid-auto-flow: dense with small row units gets you close, though it requires either fixed-ish item heights or JavaScript to calculate row spans from actual content height.
1/* Grid-based approximation of masonry, preserves DOM order better than multicol */2.masonry-grid {3 display: grid;4 grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));5 grid-auto-rows: 10px; /* small unit so spans can be granular */6 grid-auto-flow: dense;7 gap: 1.5rem;8}1// Measure each item after images load, then set a row span2// so items pack without huge gaps. Runs once on load and on resize.3function packMasonryGrid(container) {4 const rowHeight = 10; // must match grid-auto-rows5 const gap = 24; // must match the gap value in px6 const items = container.querySelectorAll('.masonry-item');78 items.forEach((item) => {9 const contentHeight = item.getBoundingClientRect().height;10 const rowSpan = Math.ceil((contentHeight + gap) / (rowHeight + gap));11 item.style.gridRowEnd = `span ${rowSpan}`;12 });13}1415window.addEventListener('load', () => {16 packMasonryGrid(document.querySelector('.masonry-grid'));17});18window.addEventListener('resize', () => {19 packMasonryGrid(document.querySelector('.masonry-grid'));20});This is more code than the columns approach and it does cause a layout recalculation on resize, which can be noticeable on pages with hundreds of items. Debounce the resize handler in production. I'd only reach for this pattern when reading order genuinely matters, for example a card feed where users tab through items sequentially.
Combining subgrid and masonry-style layouts
The two techniques compose well when a masonry-packed grid contains cards that themselves need internal alignment. You can wrap masonry-packed cards in a subgrid only if the packing container is itself a CSS grid (not multicol), since subgrid requires a grid ancestor. That's one more reason to prefer the grid-auto-flow: dense approach over columns when you also want subgrid alignment inside each card: multicol containers cannot be subgrid parents.
1/* Masonry-style packing container that also serves as a subgrid parent */2.gallery {3 display: grid;4 grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));5 grid-auto-rows: 10px;6 grid-auto-flow: dense;7 gap: 1.5rem;8}910.gallery-card {11 display: grid;12 grid-row: span 20; /* set dynamically via JS, as above */13 grid-template-rows: subgrid;14}Testing your layout across real conditions
Don't just check Chrome DevTools at one viewport width and call it done. Resize the window slowly and watch for reflow jank, particularly with the JavaScript-measured masonry approach, since resize-triggered recalculation is where most performance complaints originate. Turn on the grid overlay (Firefox DevTools has the most detailed grid inspector, showing subgrid track inheritance explicitly, which is genuinely useful for debugging broken inheritance chains).
Test with a screen reader on any layout using columns-based masonry to confirm the reading order is acceptable for your content type. Test with content of wildly different lengths, one-line captions next to five-paragraph descriptions, since that's where subgrid's alignment benefits and Masonry's packing efficiency actually get exercised. A layout that looks fine with placeholder Lorem Ipsum of uniform length tells you almost nothing about how it will behave with real user-generated content.
Finally, treat native CSS masonry as a future enhancement, not a current dependency. Track its status through the CSS Working Group's Grid Level 3 draft and revisit your fallback strategy once it ships in more than one engine. Until then, the columns and dense-grid approaches above are what you can actually ship.
Related Articles

Writing Direction-Agnostic Layouts with CSS Logical Properties
Shipping a dir='rtl' version of a site used to mean mirroring every margin-left and padding-right declaration by hand. Logical properties make direction-awareness the rendering engine's job.

Container Queries in Practice: Contextual Responsive Design
Media queries only ever answer one question: how wide is the browser window? Container queries answer a different one, and it's the one that actually matters for reusable components.

Anchor Positioning in Production: Beyond the Demo
Popper and Floating UI measure both elements on every scroll and resize event before writing inline styles. Anchor positioning moves that calculation into the browser's own layout engine.