Brand New Layouts with CSS Subgrid

When CSS Grid layout was first released, it came with a big asterisk: only the grid’s direct children could participate in the layout. “Subgrid” is a newer addition to CSS Grid which allows us to extend the grid layout down through the DOM tree.

When I first heard about subgrid, it seemed to me like a convenience, a way to make it a bit simpler to accomplish the same stuff I was already doing. As it turns out, subgrid is way more interesting than that. It opens whole new doors in terms of the UIs we can build!

In this tutorial, I’ll show you some of the exciting new things we can do with subgrid. Along the way, you’ll learn the basic mechanics of subgrid. We’ll even go over the most common gotchas!

Intended audience

This blog post assumes that you understand the basics of CSS Grid layout. If you’re not super comfortable with grid, you can learn the fundamentals here:

Credit to Kevin Powell

Link to this headingThe fundamentals

We’ll get to the interesting stuff soon, but first, let’s start with the basics.

Suppose we want to implement the following mockup:

Mockup of a portfolio UI. On the left, there's a gray box with a heading and some smaller text. On the right, there's a collection of 6 pieces of artwork. The whole thing is arranged in a 4 by 2 grid, with the gray box on the left spanning two rows.

We can create this layout using a flat grid, no subgrid required. Here’s a quick implementation:

Code Playground

Format code using Prettier

Not a responsive layout

To keep things as simple as possible, this layout is not responsive at all. Since you’re viewing this on a device with a smaller screen, you may have to scroll horizontally to view some of the layouts in this tutorial.

If we check the “Grid” devtools, we see that this is a 4x2 grid, with the header spanning the first two rows:

Screenshot of the UI shown above, with grid lines overlaid. The lines are labeled 1 through 5 horizontally, for the 4 columns, and 1 through 3 vertically, for the 2 rows.

In order for this to work without subgrid, every grid participant has to be a direct child of the .grid container. Sure enough, if we inspect the HTML, we see the following structure:

<div class="grid">
 <header>
 <h1>…</h1>
 <p>…</p>
 </header>
 <img alt="…" src="/img/thumb-sneakers.jpg" />
 <img alt="…" src="/img/thumb-rocket.jpg" />
 <img alt="…" src="/img/thumb-fish.jpg" />
 <img alt="…" src="/img/thumb-guitar-pedals.jpg" />
 <img alt="…" src="/img/thumb-machine.jpg" />
 <img alt="…" src="/img/thumb-particles.jpg" />
</div>

Semantically, this feels a bit funky to me. I feel like these images should be grouped in a list, since we’re displaying a collection of portfolio pieces. Proper semantic markup will provide more context to folks using assistive technologies like screen readers, and to search engines that are trying to make sense of our page.

Unfortunately, adding this extra markup throws a wrench into the grid:

Code Playground

Format code using Prettier

Instead of having each image occupy its own grid cell, we instead cram the entire list of images into a single cell in the second column, leaving the final two columns totally empty. 😬

CSS subgrid allows us to extend the parent grid through that <ul> tag, so that each list item (containing an image) can participate in the grid layout. Here’s what that looks like:

Code Playground

Format code using Prettier

There’s a lot going on here, so let’s unpack it.

  1. Using grid-column and grid-row, we assign the <ul> to span three columns and two rows. This is how we specify which portion of the grid we want to share with the <ul>’s descendants. We’ll dig more into this later.

  2. Next, we apply display: grid to the <ul>, to create a new child grid.

  3. Finally, we pass along the row/column definitions using grid-template-rows and grid-template-columns. The subgrid keyword is the key bit of magic that ties the two grids together, allowing each <li> to occupy its own cell in the parent grid.

A common misconception

It might seem strange that we need to set display: grid on the child <ul>; we’re not trying to create a brand-new grid, we’re trying to extend the existing grid, right?

Well, kinda. Technically, we are creating a new grid when we use subgrid, but that grid will inherit the template from the parent grid. There’s only one grid structure, but it can be shared by multiple grids.

Why did they set it up this way? I think it makes sense when we remember that CSS is a collection of different layout modes. When we apply display: grid to an element, we opt into Grid layout for that element’s children. Without that declaration, the children would use regular ol’ Flow layout, which means they wouldn’t have any awareness of the grid template.

It actually winds up being a good thing that subgrids create their own grids. It means that, for example, we can apply a different gap to the subgrid, if we want there to be more or less space between the rows/columns within the subgrid.

When I first learned about subgrid, this is the sort of scenario I was imagining: cases where nested HTML elements like <ul> + <li> or <figure> + <figcaption> block us from assigning the actual UI elements to the grid. CSS subgrid is a nifty lil’ escape hatch for these types of situations!

That said, it's not like we haven’t had other ways to solve these kinds of problems. Instead of sharing a single CSS grid template with subgrid, we could instead combine a Flexbox row with a nested grid:

Code Playground

Format code using Prettier

Instead of trying to rig everything up to use a single grid structure, we can often create the same layout with nested combinations of Flexbox/Grid. And honestly, I think I prefer this approach in this case! It feels simpler to me.

But like I said earlier, this isn’t the most exciting use case for subgrid. Now that we’ve covered the basic syntax, we can explore some of the more interesting possibilities. 😄

One of the trickiest things about CSS is that we don’t typically have great debugging tools. There is no console.log() or debugger; in CSS. We don’t even get error messages!

When it comes to CSS Grid, however, the devtools are excellent. We can enable an overlay that shows the structure of our grid. It even works for subgrid!

Across Firefox/Chrome/Safari, you can enable the grid devtools in the “Inspector”/“Elements” pane. Find the grid/subgrid in question, and click the little pill-shaped buttons to enable the overlay:

Screenshot of the “Inspector” pane in Firefox, showing the markup for the portfolio example above. A hand-written annotation says “click me!”, with arrows to the grid/subgrid buttons

Props to Firefox for leading the way on this. Chrome and Safari have added nearly-identical grid devtools, but Firefox started it all.

Link to this headingNew layout possibilities

Sticking with the artist portfolio example, let’s suppose we have this card design:

A big yellow pufferfish

Bret’s Dead Fish

I created this render for the Animation Design module in my upcoming course, Whimsical Animations(opens in new tab). The fish is a nod to Bret Victor’s talk, “Stop Drawing Dead Fish”, which is referenced in the course.

This looks alright on its own, but something funky happens when we put it in a grid:

Code Playground

Format code using Prettier

Notice that the images are different widths? The fish image, for example, is much wider than the final supercomputer image. What’s going on here? 🤔

Well, let’s take a look at the CSS. The four cards are arranged in a two-column grid (which shrinks to a one-column grid on smaller screens):

.grid {
 display: grid;
 grid-template-columns: 1fr 1fr;

 @media (max-width: 32rem) {
 grid-template-columns: 1fr;
 }
}

We’re populating this top-level grid with four <article> cards. Each card declares its own two-column grid:

.grid article {
 display: grid;
 grid-template-columns: 2fr 1fr;
}

The goal here is for the image to take up the lion’s share of the space within each card, since that’s the important part (the point of an artist’s portfolio, after all, is to showcase the art!). But the fr unit is designed to be flexible; it will try to match the requested ratio, but it’ll adapt based on the content.

This is actually a very good thing. We could force the image column to be a fixed size, but we wouldn’t like the results:

Code Playground

Format code using Prettier

On certain viewport sizes, the cards simply aren’t large enough to devote ⅔rds of the available space to the image and still contain the text content. If we force that column to have a fixed size, the text could wind up overflowing:

Screenshot showing the fourth card. The machine image is nice and wide, but the text column is spilling beyond the right edge of the card

So, the flexibility we get from the fr unit is a good thing. The problem is that each card is doing its own internal calculation. The heading in the first card (“Bret’s Dead Fish”) is made up of small words, so it can fit comfortably in a narrow column. But the final card’s heading (“Infinite Supercomputer”) requires quite a bit more room.

If you’ve worked with CSS for a while, you’ve probably gotten stuck in cul-de-sacs like this. One of the hardest problems in CSS is when siblings need to be aware of each other inside nested / complex layouts.

Miraculously, subgrid offers a solution to these sorts of problems. Check this out:

Code Playground

Format code using Prettier

How cool is this?? 🤯

In the original version, the parent grid was a one-column layout (on smaller screens), and it contained a bunch of independent grids. In this new version, the parent grid holds the two-column layout:

The grid from the playground above, showing that each card has an image which is 95.85px wide and a text column that is 96.15px wide

In the original version, the parent grid was a two-column layout, with each card assigned to a grid cell. In this new version, the parent grid grows to four columns:

The grid from the playground above, showing that the two image columns (1 and 3) are the same size at 67px. The second column is 73px, and the final column is 96px.

Each <article> will span two of these columns (grid-column: span 2), and inherits the column definitions from the parent (grid-template-column: subgrid).

As a result, the grid can dynamically react to content changes. Try erasing the word “Supercomputer” in the playground above and notice how the columns readjust!

As a result, the grid can dynamically react to content changes. If that final card (“Infinite Supercomputer”) had a shorter title, the whole grid would rearrange, shrinking the text columns and allowing more of the images to be shown.

Honestly, I’m not really used to thinking about layouts like this. Before subgrid, I might’ve solved this problem by picking a very narrow fixed width for the image column, so that there was always enough space for the text column. This would ensure that the layout never breaks, but remember, the goal of a portfolio is to display as much of the images as possible! Subgrid allows us to adapt to the content dynamically, so that we can produce the best possible UI in various contexts.

This is where subgrid truly shines, in my opinion. By extending the grid downwards, it means that we can allow siblings to become responsive to each other, in a way that hasn’t been possible until now. ✨

As I’ve been experimenting with subgrid, there have been a couple of things that have caught me off guard. Let’s go over them, so that you’ll be well-prepared!

Sharing columns with subgrid tends to be pretty intuitive, but things get a bit more quirky when sharing rows.

To help me explain, let’s look at a different example. Suppose our design team wants us to build the following pricing UI, to show the features included at different price tiers:

two cards side-by-side, listing the features included with two different packages. The text for each feature is sometimes long enough that it wraps onto a second line, but the two lists stay perfectly aligned, so that the first line of each feature is sitting on the same baseline as the equivalent feature in the opposite card

This seems like a pretty straightforward task, but the devil is in the details. If we use a typical Grid or Flexbox strategy, we’ll wind up with asymmetrical rows:

two cards side-by-side, listing the features included with two different packages. The text for each feature is sometimes long enough that it wraps onto a second line, but the two lists stay perfectly aligned, so that the first line of each feature is sitting on the same baseline as the equivalent feature in the opposite card

This might look right at a quick glance, but notice how the features don’t line up. In the original mockup, the first line of every feature is perfectly aligned with the same feature in the opposite card!

Historically, the only way to achieve this sort of thing in CSS has been with Table layout (using <table> tags, or display: table). It’s not really practical to use a table here, though, since we’d need each card to be its own column in the same table, and we can’t easily style table columns.

Subgrid to the rescue! At least in theory, we should be able to let both cards share a single grid, like this:

The mockup from earlier but with the Grid devtools overlay, showing that each feature is perfectly aligned in a row across both cards

Unfortunately, there’s a very easy mistake to make. See if you can spot the problem with this code:

Code Playground

Format code using Prettier

All of the text is clumped up in the same spot! If we inspect this using the Grid devtools, we discover that we’ve wound up with a 2×1 grid. All of the content within each card is smushed into a single row. 😬

Typically, with CSS Grid, we don’t need to explicitly define any rows. I usually define the number of columns, and trust the grid algorithm to add new rows as-needed, so that each child gets its own grid cell.

Unfortunately, with subgrid, it doesn't quite work like this. By default, our child grid will only span a single grid column/row. If we want it to occupy multiple rows, we need to reserve them explicitly.

Here’s what the fix looks like:

Code Playground

Format code using Prettier

The extra-complicated thing about this setup is that we’re extending the grid down two layers:

  • First, we extend it to <div class="card">, which includes an <h2> and a <ul>.

  • Next, we extend it to that child <ul>, so that the individual list items each get their own row.

There are 5 list items in this case, which means we need 6 rows total (one for the heading, five for the list). If we don’t “reserve” all of these rows explicitly, then the browser will shove everything into a single row and make a big mess, like we saw above.

This is mind-bending stuff, but it becomes intuitive with a bit of practice. The thing to keep in mind is that subgrids, by default, will only occupy a single grid cell. In order to spread a group of items across multiple grid rows, the subgrid must first stretch across that area itself.

Dynamic data?

In the example above, I knew exactly how many rows I needed, because the content was static. But what if we’re using dynamic data, and we don’t actually know how many rows we’ll need? And what if each column has a different number of rows??

In cases like this, there is a hacky workaround we can use:

.card,
.card ul {
 grid-row: span 99;
 display: grid;
 grid-template-rows: subgrid;
}

Instead of specifying the exact number of rows needed, we can instead pick a big number like 99, one that will definitely exceed the actual number of rows. We’ll wind up with a 99-row grid, and all of the unused rows will stack up together at the bottom.

As hacky as this feels, it’s fairly harmless; as far as I can tell, there isn’t a significant performance cost to having a bunch of empty rows, and by default, they’ll all take up 0px of height, so they won’t affect the layout at all.

The big downside to this approach is that we lose the ability to use the gap property. gap will add space between all rows, even empty ones, so if we wind up with dozens of empty rows at the bottom of our grid, we’ll also wind up with a ton of empty space.

Why doesn’t the auto-assignment algorithm work with subgrid?

When creating flat layouts with CSS Grid, we have two choices:

  1. Define an explicit number of rows using grid-template-rows on the parent (a top-down approach).

  2. Leave grid-template-rows set to the default value of auto, which will allow the grid structure to be dynamically derived from the children (a bottom-up approach).

In this case, we’d like to use the bottom-up approach, having the number of grid rows computed based on the number of children, but when we set grid-template-rows: subgrid, we’re specifically instructing the inner grids to inherit the grid structure from the parent.

In CSS, data can only be sent in one direction; either it’s set on the parent and passed down through the children, or it’s calculated by the children and bubbles up to the parent. We can’t combine approaches. Conceptually, this is the same issue we run into with percentage-based heights and with container queries.

Special thanks to Max Duval(opens in new tab) for sorting this out!

Link to this headingNested grid numbers

We got the gnarliest gotcha out of the way first! I promise the next two won’t be as intellectually taxing. 😅

In CSS grid, the lines between each column are numbered, and we can assign grid children using these numbers. This is something we explore in greater depth in “An Interactive Guide to CSS Grid”:

.child {
 grid-row: 2 / 4;
 grid-column: 1 / 4;
}

When we inherit a portion of the grid using grid-template-rows: subgrid or grid-template-columns: subgrid, the line numbers get reset.

Here’s an example of what I’m talking about:

Code Playground

Format code using Prettier

Resize editor. Use left/right arrows.

Our yellow .child is assigned to grid-column: 2 and grid-row: 2, but it winds up sitting in the third of the grid’s four rows and columns. 🤔

It turns out that while the grid template is inherited with subgrid, the line indexes don’t. Our .subgrid grid inherits columns/rows 2 through 4, but internally, they get re-indexed as 1 through 3.

We can see this using the grid devtools in the Elements inspector:

Screenshot showing the devtools enabled on both the main grid and the subgrid. The main grid has each line labeled 1 through 5. The inner grid spans from lines 2 through 5, but internally they get relabeled as 1 through 4

In my mind, I had been thinking of line numbers as unique IDs, and so I figured that if the subgrid is inheriting the grid template, those IDs would come along for the ride too. But if we think of these line numbers as indices rather than IDs, this behaviour makes a lot more sense. In every grid, the first line has index 1, even if that row/column is inherited from a parent grid.

Accessing grid areas from a subgrid?

When assigning grid children, we can do so using the line numbers (eg. grid-column: 2), but as we learned in “An Interactive Guide to CSS Grid”, we can also create named sub-sections of the grid, and assign children using these unique area names.

This got me wondering: can the elements within a subgrid still access the named areas defined by the parent grid?

Fortunately, the answer is yes. Here’s a quick example:

Code Playground

Format code using Prettier

Resize editor. Use left/right arrows.

Link to this headingIncompatibility with fluid grids

Perhaps the most famous grid snippet is this lil’ guy:

.grid {
 display: grid;
 grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
}

This is a fluid design concept. Instead of specifying different grid templates at different viewport sizes using media queries, we specify that we want as many columns as possible, as long as they’re all at least 100px wide (or whatever the minimum specified size is).

Try resizing the “Result” pane by dragging the vertical divider, and notice how the columns adjust:

Code Playground

Format code using Prettier

Resize editor. Use left/right arrows.

This is a very cool approach, but unfortunately, it doesn’t quite work with some of the new UI possibilities introduced by subgrid. For example, the “portfolio card” grid we explored earlier requires that we list the specific number of columns. We can’t use auto-fill or auto-fit.

(Or, more accurately, I haven’t found a way to use fluid design in conjunction with that subgrid pattern. If you’ve found a solution, please let me know on Bluesky!(opens in new tab))

Link to this headingSupporting older browsers

Subgrid has been supported across all major browsers since 2023. Surprisingly, though, subgrid support still hasn’t hit 90% yet (according to caniuse(opens in new tab), as of November 2025).

This presents a bit of a challenge. As we’ve seen in this blog post, subgrid enables us to solve problems that were previously unsolvable. What should we do for folks who visit using older browsers?

Well, we can’t produce an identical experience, but I think with a bit of creative problem-solving, we can come up with alternative layouts that are good enough. Using the artist portfolio example from earlier, we could reconfigure the card layout so that the image is stacked vertically, rather than horizontally:

The same UI from above, with the four artist portfolio cards showing things like a pufferfish and a large machine. This time, the images are full-width and 140px tall, sitting above the heading and paragraph.

We can accomplish this using feature queries. Here’s what the code looks like:

@supports not (grid-template-columns: subgrid) {
 .grid article {
 grid-template-columns: 1fr;
 grid-template-rows: 140px 1fr;
 }
}

Alternatively, I could have kept the two-column layout but restricted the image column’s width (eg. grid-template-columns: 50px 1fr). This would’ve preserved the original design for everyone. But I think when it comes to fallbacks, the goal isn't to be as similar to the original as possible, the goal is to produce the best experience possible. In this particular case, I think a single-column fallback experience works better.

Link to this headingThe darkest week of the year

I’m publishing this post on November 25th, a frankly miserable time of year up here in the northern hemisphere 😅. The days are getting shorter, the weather is getting colder, and my favourite season (autumn) is transmogrifying into my least favourite season (winter).

But there is one silver lining about this time of year: everything’s on sale for Black Friday! 🎈

For the past few years, my main focus has been creating comprehensive interactive online courses. I have two flagship courses, CSS for JavaScript Developers(opens in new tab) and The Joy of React(opens in new tab), and this week, they’re up to 50% off(opens in new tab)!

If you found this blog post useful, you’ll likely get so much out of my CSS course. We focus on understanding CSS at a deeper level, building an intuition for how the language actually works. No more memorizing snippets, or trying random stuff hoping that the UI will snap into the right shape!

Holy smokes, it's the Black Friday sale! Save up to 50% on my flagship courses. It’s time to change your relationship with web development.

I know that in the world of e-commerce, things go on sale every other week. That’s not how I roll, though. I only have one or two sales a year. So this truly is a rare chance to pick up one of my courses for a deep discount. ✨

You can learn more here:

Link to this headingIn conclusion

One of the coolest websites I’ve seen in a while is Stripe’s developer site(opens in new tab).

If we pop open the grid devtools, we see that the entire layout is one big grid, passed down through several layers of subgrids:

screenshot of stripe.dev, showing a 24-column grid with several subgrids, spread all across the page

This is incredibly cool, and I think it’s a great demonstration of the maximalist things we can do with subgrid. But, honestly, I think I’m more excited by the smaller-scale stuff we’ve seen in this blog post. 😅

Subgrid is a very versatile new tool, and it can be a bit intimidating and overwhelming, but hopefully this post has given you some ideas for the sorts of things you can start experimenting with. The good news is that you don’t have to re-architect your entire project in order to start using subgrid! The most powerful parts of subgrid are things which can be incrementally adopted.

Another special thanks to Kevin Powell. The examples in this blog post would’ve been far less compelling without his inspiration. 😄

ホーム - Wiki
Copyright © 2011-2025 iteam. Current version is 2.148.1. UTC+08:00, 2025-11-28 05:50
浙ICP备14020137号-1 $お客様$