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.
Archives (1) CSS (4) Conferences (4) Personal (2) Safari (1) Site (2) Thidrekssaga (1)
May 2026 (3) April 2026 (9) 2004-2021 blog
I have a grid that I want to fill with items, not row by row, as usual, but column by column. At the same time, I want to set a maximum number of columns, and add rows as needed, as a regular row-by-row grid does. To do so I created the technique described here.
Then I noted it was pretty brittle, and figured out why. I found I could not make it more robust because children-count() is not yet supported. So we’re stuck with the sub-optimal version — for now.
This is a two-parter. This first part describes the brittle solution that works right now, and the second the robust solution that doesn’t yet work.
Thanks to Temami Afif, Jane Ori, Roman Komarov, and Amit Sheen for their help with several aspects of this mini-series.
The sidebar on my blog pages contains a list of tags. I use a simple grid because the list can have either one or two columns, depending on the width of the sidebar. Simple grid is simple: it places the items in rows; the first two items in the first row, the second two in the second row etc. That’s how grids work.
I don’t like that. The tags are in alphabetical order, and intuitively I want to first fill the left column alphabetically, then continue with the next — like an index in a book.
At the same time, I want to set a number of columns in a manner similar to auto-fit, and derive the necessary number of rows from that.
Here’s how you do it. The CSS variables determine the number of columns auto-fit-style and then derive the necessary number of rows. It’s this number of rows that we pass on to the grid, hoping to get the right number of columns back. (We may not.)
.columnGrid {
--size: 150px;
--padding: 0.5em;
display: grid;
container-type: inline-size;
grid-template-columns: repeat(auto-fit,minmax(var(--size),1fr));
padding: var(--padding);
@supports (order: sibling-count()) and (order: calc(1cqw/1px)) {
grid-template-columns: 1fr;
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);
}
}
You could hard code --maxColumns: 3 or whatever value you like. If you do, you don’t need container-style or --gridWidth.
Here are a regular grid and a column-filled grid in action:
It looks great but there are rather a lot of caveats.
sibling-index/count() (unsolvable).grid-auto-flow: column is impossible because you can’t find the grid container’s width (solvable), and children-count() is not supported (unsolvable). More on that in part 2.On the positive side, this is not something you can do with flexbox.
So how does the calculation work?
--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);
gridWidth establishes the width of the grid container minus the padding. For this to work the grid container needs to be a queryable container.maxColumns divides the container width by the desired column size (in this example 150px) and rounds down. This is the maximum amount of columns the end result is going to use. (Yes, maximum. It could be fewer.)rows divides the total number of grid items (long live sibling-count()!) by the number of columns and rounds up. That’s how many rows we need in order to place all of the items.sibling-count()/index(). That’s why, even if it would survive the previous line, it fails here.row is the row number for an individual grid item. This calculation relies on modulo: the remainder of dividing the item’s sibling-index() by the total number of rows.Suppose we have three rows and items 1 to 9. 1/3, 4/3, and 7/3 have a remainder of 1, and items 1, 4, and 7 go in row 1. Similarly, 2/3, 5/3, and 8/3 have a remainder of 2 and items 2, 5, and 8 go in row 2.
Finally, 3/3, 6/3, and 9/3 have a remainder of 0 but items 3, 6 and 9 go in row 3. Since we need 1, 2, and 3, not 1, 2, and 0, add 1 to the modulo but subtract 1 from the index. Now it works.
And what’s the thing with the maximum number of columns? In grids, you can set either the number of columns or the number of rows exactly, but not both. CSS Grid needs some wiggle room somewhere.
In regular grids we set the number of columns, and leave it to the grid to decide on the number of rows. Some numbers of rows don’t occur. The regular grid above can’t have five rows.
Now let’s go to a column-filled grid. Although we start the calcuation with the number of columns, it’s actually the number of rows that we set. That means we can’t fully control the number of columns. Consider:
Here, we have nine items and --maxColumns: 3. No one will be surprised to hear that nine items in three columns need three rows. That’s what our CSS variables set, and CSS Grid does the obvious thing and generates three columns.
When we go to --maxColumns: 4, things aren’t as neat. Two rows would be too few, since 4 columns * 2 rows = 8 item cells for 9 items. So we still need three rows, and that’s what our CSS variables set.
But now that we still use three rows for nine items, three columns is enough. We don’t need the fourth column, so CSS Grid doesn’t create one. --maxColumns: 4 yields three columns.
At the time of writing Firefox doesn’t support this technique. We have to make sure Firefox users still see a regular grid. It won’t be column-filled, but that can’t be helped. So let’s give them (and in fact everyone) just that:
grid-template-columns: repeat(auto-fit,minmax(var(--size),1fr));
The auto-fitted columns take up all the available space. Thus, if the actual number of columns becomes greater than the number auto-fit calculates, there’s hardly any space left for those columns, as you can see in the example.
That’s where the @supports comes in. If the browser supports the technique, reset the number of columns to one — CSS Grid will add them as needed. Also, set the width of the one defined column as well as any automatically-created ones to 1fr.
@supports (order: sibling-count()) and (order: calc(1cqw/1px)) {
grid-template-columns: 1fr;
grid-auto-columns: 1fr;
}
Remember, Firefox lacks two features: sibling-count() and the ability to divide lengths. We check for both, and the syntax of @supports requires us to use a property: value pair. In this specific case the property doesn’t matter, but it still needs one. I picked order more or less at random, but you can use any property that accepts an integer: z-index, even grid-row.
When I arrived at this point for the first time I admired the cleverness of the technique and myself, loved the way I forced CSS Grid to do my bidding.
Then I started having doubts. I bent CSS Grid out of shape, and holes were forming. If I’d work with, rather than against, CSS Grid the technique would become more robust. Unfortunately, as we’ll see in part two, that is impossible.