Blog homepage RSS feed Mastodon Bluesky About me
This blog uses a new RSS feed. Please update the old QuirksBlog feed you used to follow.
For my recent series about column-filled grids I wrote an overview of parts of the grid algorithm. I ended up not using it in that series, but thought I’d publish it anyway.
During editing I got caught up in the spirit of things and explained why, as so often, it’s better to hard-code a relatively small number of constraints and let browsers handle the rest.
So here it goes; a simplified summary of the Grid algorithm for slightly-advanced CSS developers with lengthy detours and no real point.
As you know, a grid consists of columns and rows. Together, they are called tracks, and in some respects they work the same.
Tracks can be explicit, which means a property defines them. This snippet creates two explicit column tracks.
grid-template-columns: 1fr 1fr;
This grid has two columns and six items. We didn’t define any rows. How many rows does it have?
Well, duh. Three.
A duh is nearly always a sign of implicit tracks. The grid creates three rows because the item placement of the six items requires them. They’re here because they’re needed. You didn’t define them, but they’re implicit in the entire grid structure.
I hope the existence of grid-row and grid-column does not come as a surprise to you. They allow you to explicitly place an item in a specific row and/or column. My technique rests on explicitly assigning rows to items.
Temami pointed out something I didn’t know, or hadn’t fully realised. What does the following code do?
.grid {
grid-template-columns: 1fr 1fr;
}
.grid > :nth-child(4n) {
grid-column: 4;
}
Well, it places the 4th, 8th, etc. item in the fourth column. Duh. Makes sense, doesn’t it? You obviously want a four-column layout.
Duh again signals implicit tracks. The grid-column: 4 creates a fourth column, and the existence of a fourth column implies the existence of a third column. So the grid has a third column.
Fun challenge for semantic nerds: is the fourth column explicitly or implicitly defined? Discuss.
But why are the third and fourth column so narrow? That’s because they don’t have a defined size. The explicitly defined first and second columns do: 1fr. But the implicit third and fourth column have the default width of auto. In Grid, that’s a complicated value, but a first approximation is min-content: as little as we can get away with given the content.
So the third and fourth column get their minimally necessary width, and the first and second divide the rest of the width among them.
.grid {
grid-template-columns: 1fr 1fr;
grid-auto-columns: 1fr;
}
grid-auto-columns and -rows set a width and height for automatically-created columns and rows. Adding grid-auto-columns: 1fr solves the issue: now all columns have width 1fr and the end result is much better.
For rows, auto means "the minimum height necessary", and that’s usually the perfect height for grid rows. That’s why we generally don’t bother setting grid-auto-row.
Why does the ninth item in the example below create a third row? Why not a fifth column? What makes it prefer rows over columns?
grid-auto-flow does. It can be row (the default) or column and means something like "if you’re forced to create a new implicit track, make it a row/column." Since in our example it’s row we get a third row, and not a fifth column.
You’ve probably seen a grid like this a few times when you made a syntax error in your column definition.
If you don’t define any columns, each item creates its own row because that’s what grid-auto-flow: row says it should do.
.grid {
grid-auto-columns: 1fr;
grid-auto-flow: column;
}
The other, more rarely used value is column. Now the grid prefers to create columns for extra grid items. This simple example creates a new column for every grid item.
Let’s expand this column flow example. We use the same code as before to place the 4th, 8th etc. item in the fourth column.
.grid {
grid-auto-columns: 1fr;
grid-auto-flow: column;
}
.grid > :nth-child(4n) {
grid-column: 4;
}
A lot starts happening at once, and some of it is familiar. The fourth and eighth items are placed in column four. Like before, this creates four implicit columns, and the ninth item adds a fifth column because grid-auto-flow: column tells the grid to create extra columns when necessary; not rows.
However, the grid-column does something else as well. When placing the eighth item in the fourth column, it can’t go in the first row because the fourth item is already in row 1 / column 4. So we obviously need a second row and it’s implicitly created.
But what’s with the weird item order? Well, first we count top-to-bottom in the leftmost column, and we already saw why the example has two rows. So the placement of items 1, 2, and 3 is understandable. 4 goes in column four, even though this is actually the seventh cell in logical order. We commanded this explicitly, after all. So 5 goes in the fourth cell, 6 in the fifth, 7 in the sixth, 8 is again forced into the fourth column but happens to take up the eighth cell. 9, finally, has to create a new track, and grid-auto-flow: column makes that track a column. We gave explicit instructions, and they’re obeyed to the letter, even when they don’t really make sense.
Let’s go to the formal algorithm.
First, the grid calculates the minimum necessary number of columns and rows. This information comes from explicit sources such as grid-template-*, but also from declarations such as the grid-column: 4 we saw above.
The grid also determines from grid-auto-flow what kind of tracks to create for 'overflow' items: rows or columns. Let’s say that it’s rows; that use case is more popular and more familiar to everyone.
grid-column and a grid-row are placed in their correct column and row, creating implicit tracks if necessary.grid-row are placed in the leftmost empty position in their correct row, creating implicit tracks if necessary.grid-column, are placed, in order order, with source code order breaking any ties. However, any item with display: none is skipped.
grid-column, the pointer moves to the next empty cell in that column and places the item there. This step may move the cursor to the next row.If grid-auto-flow is column, all instances of 'row' and 'column' in the algorithm above are swapped, and 'leftmost' is replaced by 'topmost'.
I’d like to focus on step 2.
Items with a defined
grid-roware placed in the leftmost empty position in their correct row, creating implicit tracks if necessary.
Earlier, I created a technique for a filling a grid column by column (as if grid-auto-flow is column), but that also sets the maximum number of columns as if it uses a grid-template-columns with auto-fill. It works as follows:
.columnGrid {
display: grid;
grid-auto-columns: 1fr;
& > * {
--gridWidth: calc(100cqw - var(--padding) * 2);
--maxColumns: round(down,calc(var(--gridWidth) / var(--size)));
--rows: round(up,calc(sibling-count() / var(--maxColumns)));
--row: calc(mod(calc(sibling-index() - 1),var(--rows)) + 1);
grid-row: var(--row);
}
}
It first determines the maximum number of columns, and from that the amount of rows that it needs. Then it assigns the correct grid-row to every grid item.
In other words, it only uses step 2 of the grid algorithm.
And that’s a problem. It feels to me like using position: absolute all over your site. Yes, it works, but only in perfect circumstances, and as soon as anything goes wrong a lot goes wrong. The technique is brittle.
I saw that and tried to create a better technique, but it needs children-count(), which doesn’t exist yet.
It would be better if we could use a lighter touch and set only a few items to their correct row, like we did above.
.columnGrid {
display: grid;
grid-auto-columns: 1fr;
}
.grid > :nth-child(4n) {
grid-row: 4;
}
Fewer instructions, fewer things that can go wrong.
Remember, the grid now prefers to create imnplicit columns. It’s implicitly ordered to have four rows, though, so it’s only the fifth item that starts a new column.
But this version is not perfect. When we hide one item, the grid doesn’t flow nicely into the hole. That’s understandable: we told it to put the fourth item in the fourth row, and it does so.
Here’s the relevant portion of the algorithm again, but now with grid-auto-flow: column engaged:
- A 'cursor' is created that points to the topmost empty cell in the first column.
- The first unplaced item is placed there. However, if that item has a
grid-row, the pointer moves to the next empty cell in that row and places the item there. This step may move the cursor to a lower column.- Then the cursor moves to the next cell in the column, or the topmost empty cell in the next column, creating new implicit columns if necessary.
- During this entire process the cursor only moves forward, never back. Thus, there may be empty spots in the grid.
So the 'cursor' starts in the top left cell, the first item is placed there, and the cursor moves to the next cell down. The second item is skipped because it has display: none. The third item is placed in the second cell. Then the fourth item is placed in row 4, and the third cell is skipped. Since it was calculated there’s a maximum of four rows, the cursor goes to the next column and places the remaining items there.
This is better than the original technique, which goes wrong even more spectacularly because every item is forced into a row, whether that makes sense or not.
Still, it’s not perfect. The original technique placed all items in a specific row. The looser technique only places a few items in a row. That is better, but in both cases we don’t check whether the item placement makes sense (and, to be honest, I’m not sure that’s possible in CSS alone).
So far we tried to tell items what to do instead of programming a few constraints and then go out of the way and let browsers handle the exact placement. We should just tell browsers how many rows or columns we need, but don’t handle individual items. The next example does that.
.columnGrid {
display: grid;
grid-template-rows: repeat(4,1fr);
grid-auto-flow: column;
grid-auto-columns: 1fr
}
This works best. Four rows are created, and the items fill up these rows neatly. Item 4 is in the third cell, as it should be when item 2 is hidden. Unfortunately, as I explained earlier, I can’t calculate how many rows my technique needs because of the lack of children-count(). But if you can hard-code the number of rows this is the best technique.
As is so often the case in web development, the best way to create a layout is to give as few instructions as possible and then get out of the way and let browsers sweat the details. Grid, like many other CSS modules, was written with a lot of sensible defaults in place. As long as you don’t overrule those defaults too much, your site will behave well even in adverse circumstances.