{"id":432,"date":"2026-07-08T05:03:22","date_gmt":"2026-07-08T05:03:22","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/css-subgrid-2026-the-nested-grid-alignment-i-actually-use\/"},"modified":"2026-07-08T05:03:22","modified_gmt":"2026-07-08T05:03:22","slug":"css-subgrid-2026-the-nested-grid-alignment-i-actually-use","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/css-subgrid-2026-the-nested-grid-alignment-i-actually-use\/","title":{"rendered":"CSS Subgrid in 2026: The Nested Grid Alignment I Actually Use"},"content":{"rendered":"<p>Okay, this is going to sound dumb, but I lost about three hours last month trying to align a card grid: titles, meta rows, buttons, all sitting at the same vertical position across cards of different lengths. I was about halfway into writing JavaScript to measure the tallest title and set a <code>min-height<\/code>, when a colleague pinged me in Slack: &ldquo;you know CSS Subgrid exists, right?&rdquo; Reader, I did not. Well, I knew the word. I&rsquo;d read a blog post about it in 2023 and filed it under &ldquo;maybe useful someday&rdquo;. It turns out someday was that afternoon.<\/p>\n<p>This post is the version of that blog post I wish I&rsquo;d read: no history lesson, no browser support timeline you can look up on Can I Use in five seconds, just the pattern I now reach for and the two footguns I hit while learning it.<\/p>\n<h2 id=\"what-subgrid-actually-solves\">What subgrid actually solves<\/h2>\n<p>Here&rsquo;s the problem. You have a card grid. Each card has a title, a body, and a footer with a button. When the titles have different lengths, the bodies start at different vertical positions. When the bodies have different lengths, the buttons end up on different rows. Users notice. Designers <em>definitely<\/em> notice.<\/p>\n<p>Before subgrid, my options were:<\/p>\n<ol>\n<li>Make every title the same height with <code>line-clamp<\/code> and pray about long titles.<\/li>\n<li>Set a <code>min-height<\/code> on each section, which meant either a lot of white space on short content or clipping on long content.<\/li>\n<li>JavaScript. Measure the tallest of each row, apply a fixed height. Reruns on resize. This is what I was about to write.<\/li>\n<\/ol>\n<p>Subgrid lets a child element opt into its parent&rsquo;s grid tracks, so the title row, body row, and footer row across all cards line up automatically. No measurement, no JavaScript, no clipping. That&rsquo;s the whole idea.<\/p>\n<h2 id=\"the-grid-template-rows-subgrid-line-that-changed-my-week\">The <code>grid-template-rows: subgrid<\/code> line that changed my week<\/h2>\n<p>Here&rsquo;s the shape. The outer grid defines the columns and, crucially, the row structure. Each card spans three rows and inherits those row tracks with <code>grid-template-rows: subgrid<\/code>.<\/p>\n<pre><code class=\"language-css\">.card-grid {\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));\n  grid-template-rows: repeat(3, auto);\n  gap: 1rem;\n}\n\n.card {\n  display: grid;\n  grid-row: span 3;\n  grid-template-rows: subgrid;\n}\n\n.card &gt; .title  { grid-row: 1; }\n.card &gt; .body   { grid-row: 2; }\n.card &gt; .footer { grid-row: 3; }\n<\/code><\/pre>\n<p>The magic word is <code>subgrid<\/code> on <code>grid-template-rows<\/code>. The child card is now a participant in the outer grid&rsquo;s row layout. The three rows are sized by the tallest title, the tallest body, and the tallest footer across the whole grid. Every card&rsquo;s title, body, and footer end up on the same baseline. It looks like you spent a long afternoon fine-tuning heights. You didn&rsquo;t.<\/p>\n<p>If you also want the <em>columns<\/em> to align across nested content (a metadata row inside the card where the icon should line up with the icons in every other card, for example), you can add <code>grid-template-columns: subgrid<\/code> too, though it&rsquo;s rarer than the row case.<\/p>\n<h2 id=\"before-and-after-in-a-real-project\">Before and after, in a real project<\/h2>\n<p>Here&rsquo;s what my card grid looked like before I stopped pretending subgrid didn&rsquo;t exist:<\/p>\n<pre><code class=\"language-html\">&lt;div class=&quot;card-grid&quot;&gt;\n  &lt;article class=&quot;card&quot;&gt;\n    &lt;h3 class=&quot;title&quot;&gt;Short title&lt;\/h3&gt;\n    &lt;p class=&quot;body&quot;&gt;A body of a few words.&lt;\/p&gt;\n    &lt;button class=&quot;footer&quot;&gt;Read more&lt;\/button&gt;\n  &lt;\/article&gt;\n  &lt;article class=&quot;card&quot;&gt;\n    &lt;h3 class=&quot;title&quot;&gt;A slightly longer title that wraps to two lines&lt;\/h3&gt;\n    &lt;p class=&quot;body&quot;&gt;A body that goes on for a bit longer than the first one, wrapping across a couple of lines and creating a taller card.&lt;\/p&gt;\n    &lt;button class=&quot;footer&quot;&gt;Read more&lt;\/button&gt;\n  &lt;\/article&gt;\n&lt;\/div&gt;\n<\/code><\/pre>\n<pre><code class=\"language-css\">\/* Before: cards are flex columns, buttons end up at different heights *\/\n.card-grid {\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));\n  gap: 1rem;\n}\n.card {\n  display: flex;\n  flex-direction: column;\n  gap: 0.5rem;\n}\n.card .footer { margin-top: auto; } \/* the classic &quot;push me down&quot; hack *\/\n<\/code><\/pre>\n<p>The <code>margin-top: auto<\/code> trick pushes the button to the bottom of <em>each individual card<\/em>, so buttons align on a per-card basis but not across the grid. Cards with shorter content have a big gap in the middle. Not great.<\/p>\n<p>With subgrid, the same HTML becomes:<\/p>\n<pre><code class=\"language-css\">\/* After: real cross-card alignment *\/\n.card-grid {\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));\n  grid-template-rows: repeat(3, auto);\n  gap: 1rem 1rem;\n}\n.card {\n  display: grid;\n  grid-row: span 3;\n  grid-template-rows: subgrid;\n  gap: 0.5rem;\n  padding: 1rem;\n  border: 1px solid var(--hairline);\n  border-radius: 0.5rem;\n}\n<\/code><\/pre>\n<p>The titles, bodies, and footers now share three global rows sized by the tallest instance of each. Buttons line up across the grid. Zero JavaScript. This is what I would have shipped six months ago if I&rsquo;d remembered <code>subgrid<\/code> was a real thing I could type.<\/p>\n<h2 id=\"forms-are-the-sneaky-killer-app\">Forms are the sneaky killer app<\/h2>\n<p>Card grids are the obvious pitch, but the place I&rsquo;ve used subgrid the most since is forms. If you&rsquo;ve ever tried to make a form where labels are in one column and inputs are in another, and the whole thing has to look tidy across sections and repeats, you know the pain.<\/p>\n<pre><code class=\"language-css\">form {\n  display: grid;\n  grid-template-columns: max-content 1fr;\n  gap: 0.5rem 1rem;\n}\n.field {\n  display: grid;\n  grid-column: span 2;\n  grid-template-columns: subgrid;\n  align-items: baseline;\n}\n.field label { grid-column: 1; }\n.field input, .field select { grid-column: 2; }\n<\/code><\/pre>\n<p>The field wrapper participates in the parent grid&rsquo;s two columns. Labels line up on the left, inputs on the right, and it doesn&rsquo;t matter how you group fields with <code>&lt;fieldset&gt;<\/code> or <code>&lt;div class=\"field\"&gt;<\/code>. Every label sits under every other label. It&rsquo;s the layout I used to build with a bunch of manual widths and <code>text-align: right<\/code>, and it&rsquo;s the layout that would fall apart the moment a translator turned a two-word label into a five-word label. Subgrid makes it robust.<\/p>\n<h2 id=\"what-still-breaks-and-how-i-work-around-it\">What still breaks and how I work around it<\/h2>\n<p>Three things bit me in the first week:<\/p>\n<ol>\n<li><strong>You have to remember the <code>span<\/code>.<\/strong> If your card is <code>grid-row: subgrid<\/code> without also spanning multiple rows, it takes only one row and the whole thing collapses. Always pair <code>grid-template-rows: subgrid<\/code> with an explicit <code>grid-row: span N<\/code>.<\/li>\n<li><strong><code>gap<\/code> on the child grid is ignored.<\/strong> The gap comes from the parent. This one confused me for a while. I was setting <code>gap: 1rem<\/code> inside the card and getting nothing. Move it to the parent grid.<\/li>\n<li><strong>Nested subgrids get weird fast.<\/strong> Don&rsquo;t nest a subgrid inside a subgrid inside a subgrid unless you have a very good reason. Two levels is usually the sweet spot. Beyond that, debugging becomes archaeology, and you&rsquo;re better off restructuring the layout.<\/li>\n<\/ol>\n<p>Browser support isn&rsquo;t the problem it once was. All the modern engines have shipped subgrid: Firefox got there first, Safari followed, and Chrome finally landed it in 117. The <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/CSS_grid_layout\/Subgrid\" rel=\"nofollow noopener\" target=\"_blank\">MDN page on subgrid<\/a> has the exact matrix, and the <a href=\"https:\/\/web.dev\/articles\/css-subgrid\" rel=\"nofollow noopener\" target=\"_blank\">web.dev article on subgrid<\/a> is the best conceptual explainer I&rsquo;ve found. If you&rsquo;re supporting a browser matrix that includes something older than Chrome 117, you&rsquo;ll want a <code>@supports<\/code> fallback that uses flexbox and a <code>min-height<\/code>; annoying, but not catastrophic.<\/p>\n<h2 id=\"what-id-try-next-weekend\">What I&rsquo;d try next weekend<\/h2>\n<p>If you&rsquo;ve been shipping card layouts with <code>min-height<\/code> hacks or measuring things in JavaScript, this weekend try one thing: pick a component in your app, open the CSS, and see if the layout is really &ldquo;cards in a grid where corresponding rows should align&rdquo;. If it is, delete the <code>min-height<\/code> and add <code>grid-template-rows: subgrid<\/code> on the child with an explicit <code>span<\/code>. If it looks right, ship it. If it looks wrong, the debugging story is much shorter than you&rsquo;d expect because subgrid is easy to inspect in DevTools; Firefox in particular has a grid inspector that highlights the tracks.<\/p>\n<p>I wrote about a similar &ldquo;stop writing JavaScript for a CSS problem&rdquo; pattern in <a href=\"https:\/\/abrarqasim.com\/blog\/css-has-selector-javascript-hacks-i-finally-deleted\" rel=\"noopener\">how the <code>:has()<\/code> selector let me delete a pile of JavaScript hacks<\/a>. Subgrid is the same kind of win, applied to layout rather than state. You can see it working on a real card grid on my <a href=\"https:\/\/abrarqasim.com\" rel=\"noopener\">portfolio at abrarqasim.com<\/a>; the featured work section uses exactly the pattern in this post.<\/p>\n<p>One concrete action for the week: go find the tallest <code>min-height<\/code> value in your codebase and check whether the element it&rsquo;s attached to lives inside a grid. If it does, you probably have a subgrid opportunity, and about ten lines of CSS will replace whatever it&rsquo;s currently doing.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>CSS Subgrid finally solved my card-grid alignment problem without JavaScript. Here&#8217;s the before\/after code I use in production, plus what still breaks.<\/p>\n","protected":false},"author":2,"featured_media":431,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"CSS Subgrid finally solved my card-grid alignment problem without JavaScript. Here's the before\/after code I use in production, plus what still breaks.","rank_math_focus_keyword":"css subgrid","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[137,138],"tags":[37,470,469,38,350,435,471,172],"class_list":["post-432","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-css","category-frontend","tag-css","tag-css-grid","tag-css-subgrid","tag-frontend","tag-html","tag-responsive-design-2","tag-subgrid","tag-web-development-2"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/432","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=432"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/432\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/431"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=432"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=432"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=432"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}