The pain of building nested grids

Jan 13, 2022

It feels ungrateful to complain about building layouts in 2022, since CSS has advanced so much, but you gotta do what you gotta do.


Layouting a website is not an easy task, even with the help of the CSS greats: ✨ Flexbox ✨ and ✨ Grid ✨. Although those two offer you a variety of solutions for different problems, you still have a lot of thinking to do when it comes to consistently positioning elements.

Today, we will verify how difficult it is to maintain the same grid across multiple components of various HTML depths.


👨‍🏫 Agenda:

  1. The problem
  2. Current solutions
  3. CSS Subgrid
  4. JavaScript

Useful links:


1. The problem

Let's say we want to build a grid consisting of two equal columns: A and B. The B column, however, contains another grid with 1:2 ratio (named, respectively, B1 and B2). To wrap our heads around it easier, let's draw it:

A drawing of our columns

We built our grid with 12 columns in mind, so if we were to outline them on top of our layout, we would get:

A drawing of our columns with grid outline on it

Both A and B get 6 columns each, but B also splits to B1 (2 columns) and B2 (4 columns)

Achieving it in CSS is nothing demanding, but it teases a problem that will be common across this entire piece. Let's do a quick recap of how does CSS Grid work to learn what the problem is.


CSS grid offers two sets of properties accessible from two entry points: one to set on a parent level, and another to utilize on a children level.

It goes like this - we declare the details of our rows and columns:

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

and then if we have some specific demands, we can set it up for each individual grid child:

.grid-item:first-child {
grid-column: 1 / 4;
}

In our case, however, the latter is not necessary. A grid of two equal columns (A + B) could look like this:

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

Implementing the inner grid of B1 and B2 is nothing challenging either:

.sub-grid {
display: grid;
grid-template-columns: 1fr 2fr;
}
.main-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-column-gap: 5px;
}

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

But what happens when the ratio between A and B changes?

Woops! We forgot to add the gap on the subgrid. Add it 🪄

We were lucky to choose a ratio for our B1 + B2 subgrid that plays nicely with our main grid. If we assume "1fr" from .main-grid equals to 6 columns, it divides by 3 (1 + 2, from .sub-grid's grid-template-columns property).


We can't say we benefit fully from a grid design, if each time we go deep down one level, we have to manually count how many columns we have on our disposal. The ratio from our example would go down the drain as soon as we messed up the numbers, just like so:

.sub-grid {
display: grid;
grid-template-columns: 3fr 5fr; /* <- 3 + 5 = 8; 6 / 8 = ⛔ */
}

Anything we established on a parent level, we lose track of the moment we head to a grid child. In a result, we have to babysit each subgrid we create, even though they are all based on a one, primary grid.

A graph of grid inheritance

The "main-grid" influence spreads only to its direct children: the "grid-items".

A graph of grid inheritance that doesn't reach the subgrid

We want the subgrid children to participate in the main grid, but it has no power here.


Well, the ratio just won't change on its own, right? You must willingly allow it, and that may seem unlikely in your scenario. That, however, proves that our subgrid is not a solid construction that truly determines the layout for all its children, unless our HTML is completely flat. We are doomed to live the life of a grid watchman, making sure that the ratio of columns we set on a parent level is being restricted on a children level.

2. Current solutions

2.1 display: contents

The remedy for flattening our HTML while building more complex grids can be found (in specific cases) in display: contents property. What it does is that it makes the element hold a purely semantical value, that gets ignored in terms of display, exposing its children instead.

Don't know what the hell am I talking about? I hope this example will suffice!

.articles {
  display: grid;
  grid-gap: 10px;
  grid-template-columns: repeat(3, 1fr);
}

/* .pass-grid {
  display: contents;
} */

Felt frisky and peeked into the dev tools anyway? This is what you might have seen there:

The result of inspecting the display: "contents" elements.

Did you notice how there is no box for the pass-grid div? This is the superpower that display: contents gives us: it passes on the display properties.

We can say that display: contents employs an element as a layout proxy. The job is so taxing, an element can't do anything else at the same time, meaning all the other styling properties get canceled from the element the moment we set display: contents on it.

.layout-proxy {
background: red; /* <- this works! */
}
.layout-proxy {
background: red; /* <- this no longer works */
display: contents;
}

As we can see, this is not ideal. It does not solve all the problems you can encounter while developing a complex grid. The only help it provides is with flattening the markup, but even that, to some extent.

2.2 CSS Grid powered by CSS Variables

If we can't summon our main grid anywhere in our markup, the least we can do is store the details of it in CSS Variables, and reuse it:

:root {
  --main-grid-first-col: 256px;
  --main-grid-second-col: 2fr;
  --main-grid-third-col: 3fr;
  --main-grid-gap: 10px;
}

.main-grid {
  display: grid;
  grid-template-columns:
    var(--main-grid-first-col) var(--main-grid-second-col)
    var(--main-grid-third-col);
  grid-gap: var(--main-grid-gap);
}

.aligned-grid {
  display: grid;
  grid-template-columns: var(--main-grid-first-col) auto;
  grid-gap: var(--main-grid-gap);
}
      

We can adjust the first column width

This solution proves worthy when trying to sync grids together but it does not directly bind one grid with another, just their dimensions. As far as I know, this is where our CSS possibilities end. At least for now.

3. CSS Subgrid

Hopefully, the life of a front-end developer will become a bit easier when CSS Subgrid truly arrives. For now, the only browser it's available in is Firefox, which means we can sit back and fantasize.


CSS Subgrid is a value of grid-template-columns and grid-template-rows that enables the parent to share its grid lines with its children.

We can easily highlight its benefits on the example from the beginning of the article:

A drawing of our columns with grid outline on it

Instead of effectively creating two unrelated grids that align with each other by sheer luck, with CSS Subgrid we will be able to do something like this:

<body>
<section class="main-grid">
<div class="grid-a">A</div>
<div class="grid-b">
<div class="sub-grid-item-b1">B1</div>
<div class="sub-grid-item-b2">B2</div>
</div>
</section>
</body>
.main-grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
}
.grid-a {
grid-column: 1 / 7; /* <- where "7" is the end of 6th column */
}
.grid-b {
grid-column: 7 / 13; /* <- where "13" is the end of 12th column */
display: grid;
grid-template-columns: subgrid;
}
.sub-grid-item-b1 {
grid-column: 1 / 3; /* <- where "3" is the end of 2nd column */
}
.sub-grid-item-b2 {
grid-column: 3 / 7; /* <- where "7" is the end of 6th column */
}

💻 Click here, if you have Firefox installed.


The ability to refer the .main grid's dimensions from the sub-grid-item is a true gamechanger. This opens the door to implementing and enforcing a one true grid system across the entire layout, not just create a bunch of grids that try to work together.

There is a one limitation of CSS Subgrid, though. It does not give us a way to declare a main grid, and then reference it at any depth of our markup. At some point, we would like to break the cascade...

A graph of how the grid can be inherited right now, and how it would be nice to inherit it.

CSS Grid wishful thinking

...but that's not possible. It's not unusual, though, because CSS only takes care of positioning the elements in relation to each other. To achieve it, we would have to track the state of our main grid across the entire application, and react when something's changed.

4. JavaScript

Since we are so good at fantasizing, let's do it one last time.

A perfect grid solution would allow us to:

  • enforce one grid across the entire layout
  • maintain the state of the grid, and prevent exceeding the defined number of columns/rows (with CSS Grid nothing will stop us, if we order an element to span from the column 1 to 14, even if we defined our grid as 12 columns)
  • position an element as a part of the grid, without worrying about the depth of HTML and passing the grid properties via subgrid

None of these points are currently achievable with just CSS and HTML, nor will they be with an addition of CSS Subgrid. Don't get me wrong - CSS Subgrid will definitely make our lives significantly easier, but some pains of building a complex, universal grid will remain.

However, as we've learned on numerous occasions, there are no pains in the front-end development JavaScript can't solve...

import { useGrid } from "@peelar/react-grid";

Make sure not to miss an update on that!


Previously on peelar.dev 🤓:


TwitterLinkedInGitHub