Ever wanted to get the number of auto-fit
/auto-fill
columns in a grid? For example, because you want to highlight just the items in the first or last row or column? Do something special just for even or for odd rows or columns (e.g. zebra striping)? Or for any one specific row or column? Create responsive non-rectangular grids? And all of this with zero breakpoints?
This is all doable with pure CSS by using container query units, CSS variables, and CSS mathematical functions! Of course, it also involves navigating browser bugs and support gaps. But at the end of the day, it is possible to do it cross-browser!
Let’s see how!
The Basic Idea
Setup
We start with a .grid
with a lot of items, let’s say 100
. I normally prefer to generate them in a loop using a preprocessor to avoid clutter in the HTML and to make it easy to change their number, but it’s also possible to do so using Emmet. For the demos illustrating the concept here, we’re using Pug, and also numbering our items via their text content:
.grid
- for(let i = 0; i < 100; i++)
.item #{i + 1}
Our .grid
has auto-fit
columns:
.grid {
--u: 7em;
display: grid;
grid-template-columns: repeat(auto-fit, var(--u));
container-type: inline-size
}
Code language: CSS (css)
This means our .grid
has as many columns of unit width u
as can fit within its own content-box
width. This width is flexible and is given by the page layout, we don’t know it. However, its children (the .item
elements) can know it as 100cqw
in container query units. To have these container units available for the .grid
element’s children (and pseudos), we’ve made the .grid
an inline container.
This should work just fine. And it does, in both Chrome and Firefox. However, if we try it out in Safari, we see our .grid
is collapsed into a point. Unfortunately, in Safari, auto-fit
grids break if they are also containers. (Note: this Safari bug is actually fixed, it’s just waiting to make it’s way to a stable release.)
We have two options in this case.
The first would be to replace auto-fit
with auto-fill
. When we have as many items as we do in this case, we can use either of them, the difference between them is only noticeable when we don’t even have enough items to fill one row.
.grid {
--u: 7em;
display: grid;
grid-template-columns: repeat(auto-fill, var(--u));
container-type: inline-size
}
Code language: CSS (css)
The second would be to put the .grid
inside a wrapper .wrap
element and move the container
property on the wrapper.
.wrap { container-type: inline-size }
.grid {
--u: 7em;
display: grid;
grid-template-columns: repeat(auto-fit, var(--u))
}
Code language: CSS (css)
We’re going for the first option here.
Now we’re getting to the interesting part!
Getting the number of columns
In theory, we could get the number n
of columns on the .item
children of the .grid
via division, whose result we round down (if the container width of 100cqw
is 2.23 times the unit width u
of a column, then we round down this ratio to get the number of columns we can fit, which is 2 in this case):
--n: round(down, 100cqw/var(--u))
Code language: CSS (css)
In practice, while this should work, it only works in Safari (since Sept 2024) in Chrome (since June 2025), and where we can test it out by displaying it using the counter hack:
.grid::before {
--n: round(down, 100cqw/var(--u));
counter-reset: n var(--n);
content: counter(n)
}
Code language: CSS (css)
We’ve wrapped this inside a @supports
block so we have a message that lets us know about this failing in non-supporting browsers (basically Firefox), where we see the following:

In Safari and Chrome, things look like in the recording below:
We can see we have a problem when we have one column and it overflows the parent: the ratio between the parent .grid
width of 100cqw
and the column unit width u
drops below 1
, so we can fit one item 0
times inside the content-box
width of the .grid
. And this is reflected in the n
value, even though, in practice, we cannot have a grid with less than one column. However, the fix for this is simple: use a max()
function to make sure n
is always at least 1
.
--n: max(1, round(down, 100cqw/var(--u)))
Code language: CSS (css)
Whenever the division result drops below 1
, the result of the max()
function isn’t the round()
value anymore, but 1
instead.
You can see it in action in demo below, but keep in mind it can only compute the number of columns in supporting browsers (Safari/Chrome):
Great, but what Firefox? The Firefox bug looks like it’s dormant, so we cannot get the ratio between two length values there.
Extending support
However, we have a clever hack to solve the problem!
The idea behind is the following: the tangent of an acute angle in a right triangle is the ratio between the length of the cathetus opposing the angle and the length of the cathetus adjacent to it. So basically, the tangent is a ratio between two length values and such a ratio is precisely what we need.

Now you may be wondering what right triangle and what angle do we even have here. Well, we can imagine building a triangle where a cathetus has the same length as the .grid
parent’s content-box
width (100cqw
on the .item
elements, which we’ll call w
) and the other has the same length as the column unit width (u
).
The tangent of the angle opposing the cathetus of length w
is the ratio between w
and u
. Okay, but what is this angle?

We can get this angle using the atan2()
function, which takes two arguments, the length of the opposing cathetus w
and the length of the adjacent cathetus u
:
--a: atan2(var(--w), var(--u))
Code language: CSS (css)
Having the angle a
and knowing that the ratio f
between w
and u
is the tangent of this angle, we can write:
--f: tan(var(--a))
Code language: CSS (css)
Or, replacing the angle in the formula:
--f: tan(atan2(var(--w), var(--u)))
Code language: CSS (css)
In general, know that a length ratio like w/u
can always be computed as tan(atan2(w, u))
.
Rounding down this ratio f
gives us the number of columns of unit width u
that fit within the .grid
parent’s content-box
width w
.
--n: round(down, var(--f))
Code language: CSS (css)
So we can write it all as follows, introducing also the correction that the number of columns needs to be at least 1
:
--f: tan(atan2(var(--w), var(--u)));
--n: max(1, round(down, var(--f)))
Code language: CSS (css)
That’s it, that’s the formula for --n
in the case when we don’t have support for getting the ratio of two length values! There is one catch, though: both --w
and --u
have to be registered as lengths in order for atan2()
to work properly!
Putting it all together, the relevant code for our demo looks as follows:
.grid {
--u: 7em;
display: grid;
grid-template-columns: repeat(auto-fill, var(--u));
container-type: inline-size
}
.grid::before, .item {
--w: 100cqw;
--f: var(--w)/var(--u);
--n: max(1, round(down, var(--f)));
}
@supports not (scale: calc(100cqh/3lh)) {
@property --w {
syntax: '<length-percentage>';
initial-value: 0px;
inherits: true
}
@property --u {
syntax: '<length-percentage>';
initial-value: 0px;
inherits: true
}
.grid::before, .item { --f: tan(atan2(var(--w), var(--u))) }
}
Code language: CSS (css)
Note that the .grid
pseudo is only needed to display the --n
value (using the counter hack) for us to see in the demo without having to register it and then look for it in DevTools (which is the tactic I most commonly use to check the computed value of a CSS variable).
Almost there, but not exactly.
Fixing tiny issues
If you’ve played with resizing the demo above, you may have noticed something is off in Firefox at times. At certain points when the .grid
element’s content-box
width w
is a multiple of the unit column width u
, for example, when w
computes to 1008px
and the unit column with u
of 112px
fits inside it exactly 9
times, Firefox somehow computes the number of columns as being smaller (8
instead of 9
, in this example).
My first guess was this is probably due to some rounding errors in getting the angle via atan2()
and then going back from an angle to a ratio using tan()
. Indeed, if we register --f
so we can see its value in DevTools, it’s displayed as 8.99999
in this case, even though 1008px/112px
is exactly 9
.

So this means rounding down f
results in the number of columns n
being computed as 8
, even though it’s actually 9
. Hmm, in this case, it might be better to round f
to a tiny precision of .00001
before rounding it down to get the number of columns n
:
--f: round(tan(atan2(var(--w), var(--u))), .00001)
Code language: CSS (css)
This seems to get the job done.
Still, I’m a bit worried this still might fail in certain scenarios, even though I’ve kept resizing obsessively in Firefox and haven’t encountered any problems after rounding f
.
So let’s make sure we’re on the safe side and place the .grid
in a wrapper .wrap
, make this wrapper the container
, compute the number of columns n
on the .grid
and use it to set the grid-template-columns
. This way, the essential CSS becomes:
.wrap {
container-size: inline-type;
}
.grid {
--w: 100cqw;
--u: 7em;
--f: var(--w) / var(--u);
--n: max(1, round(down, var(--f)));
display: grid;
grid-template-columns: repeat(var(--n), var(--u));
justify-content: center;
}
@supports not (scale: calc(100cqh / 3lh)) {
@property --w {
syntax: "<length-percentage>";
initial-value: 0px;
inherits: true;
}
@property --u {
syntax: "<length-percentage>";
initial-value: 0px;
inherits: true;
}
.grid {
--f: round(tan(atan2(var(--w), var(--u))), 0.00001);
}
}
Code language: CSS (css)
Note that we may also use 1fr
instead of var(--u)
for the grid-template-columns
property if we want the .item
elements to stretch.
gap
Mind the Nice, but oftentimes we also want to have a gap in between our rows and columns, so let’s see how the number of columns can be computed in that case.
Whenever we have n
columns, we have n - 1
gaps in between them.
This means that n
times the unit column width plus (n - 1)
times the gap space adds up to the container’s content-box
width:
n·u + (n - 1)·s = w
If we add s
on both sides in the equation above, we get:
n·u + (n - 1)·s + s = w + s ⇒
n·u + n·s - s + s = w + s ⇒
n·u + n·s = w + s ⇒
n·(u + s) = w + s ⇒
n = (w + s)/(u + s)
Putting this into CSS, our ratio looks as follows:
(var(--w) + var(--s))/(var(--u) + var(--s))
Code language: CSS (css)
Note that in our case, it’s the fraction f
that we compute this way before we round it to get the number of items n
and ensure n
is always at least 1
.
Also note that the CSS variables we need to register for the no calc()
length division fallback are the numerator and denominator of this fraction. So our essential CSS becomes:
.wrap {
container-size: inline-type;
}
.grid {
--w: 100cqw;
--u: 7em;
--s: 3vmin;
--p: calc(var(--w) + var(--s)); /* numerator */
--q: calc(var(--u) + var(--s)); /* denominator */
--f: var(--p) / var(--q);
--n: max(1, round(down, var(--f)));
display: grid;
grid-gap: var(--s);
grid-template-columns: repeat(var(--n), 1fr);
}
@supports not (scale: calc(100cqh / 3lh)) {
@property --p {
/* numerator */
syntax: "<length-percentage>";
initial-value: 0px;
inherits: true;
}
@property --q {
/* denominator */
syntax: "<length-percentage>";
initial-value: 0px;
inherits: true;
}
.grid {
--f: round(tan(atan2(var(--p), var(--q))), 0.00001);
}
}
Code language: CSS (css)
Let’s Go Wild!
And let’s see where we can use this!
Highlighting items on a certain column
In order to do something like this, we use the item indices. Once sibling-index()
is supported cross-browser, we’ll be able to do this:
.item { --i: calc(sibling-index() - 1) }
Code language: CSS (css)
Note that we need to subtract 1
because sibling-index()
is 1
-based and we need our index i
to be 0
-based for modulo and division purposes.
Until then, we add these indices in style
attributes when generating the HTML:
.grid
- for(let i = 0; i < 100; i++)
.item(style=`--i: ${0}`) #{i + 1}
Let’s say we want to highlight the items on the first column. We get the number of columns n
just like before. An item is on the first column if i%n
(which gives us the 0
-based index of the column an item of index i
is on) is 0
. Now given I used the word if there, you might be thinking about the new CSS if()
function. However, we have a way better supported method here.
If the column index i%n
is 0
, then min(1, i%n)
is 0
. If the column index i%n
isn’t 0
, then min(1, i%n)
is 1
. So we can do the following:
.item {
--nay: min(1, mod(var(--i), var(--n))); /* 1 if NOT on the first column */
--yay: calc(1 - var(--nay)); /* 1 if on the first column! */
}
Code language: CSS (css)
So then we can use --yay
to highlight the items on the first column by styling them differently, for example by giving them a different background
:
.item {
--nay: min(1, mod(var(--i), var(--n))); /* 1 if NOT on the first column */
--yay: calc(1 - var(--nay)); /* 1 if on the first column! */
background: color-mix(in srgb, #fcbf49 calc(var(--yay)*100%), #dedede)
}
Code language: CSS (css)
You can see it in action in the live demo below:
Now let’s say we want to highlight the items on the last column. In this case, the column index i%n
is n - 1
, which means that their difference is 0
:
(n - 1) - (i%n) = 0
Using this, we can do something very similar to what we did before, as the minimum between 1
and this difference is 0
for items on the last column and 1
for those that aren’t on the last column:
.item {
/* 1 if NOT on the last column */
--nay: min(1, (var(--n) - 1) - mod(var(--i), var(--n))));
/* 1 if on the last column! */
--yay: calc(1 - var(--nay));
}
Code language: CSS (css)
For example, if n
is 7
, then the column index i%n
can be 0
, 1
, … 6
and n - 1
is 6
. If our item of index i
is on the last column, then its column index i%n = i%7 = 6
, so the difference between n - 1 = 7 - 1 = 6
and i%n = i%7 = 6
is 0
. If our item of index i isn’t on the last column, then its column index i%n = i%7 < 6
, so the difference between n - 1 = 6
and i%n < 6
is 1
or bigger. Taking the minimum between 1
and this difference ensures we always get either 0
or 1
.
In general, if we want to highlight a column of index k
(0
-based, but we can just subtract 1
in the formula below if it’s given 1
-based), we need to compute the difference between it and i%n
(the column index of an item of index i
), then use the absolute value of this difference inside the min()
:
.item {
--dif: var(--k) - mod(var(--i), var(--n));
--abs: abs(var(--dif));
--nay: min(1, var(--abs)); /* 1 if NOT on column k */
--yay: calc(1 - var(--nay)); /* 1 if on column k! */
}
Code language: CSS (css)
The difference and its absolute value are 0
when the item of index i
is on column k
and different (bigger in the case of absolute value) when it isn’t.
We need the absolute value here because, while the difference between n - 1
and i%7
is always 0
or bigger, that is not the case for the difference between any random k
and i%n
. For example, if n
is 7
and k
is 2
, the k - i%n
difference is negative when k
is smaller than i%n
, for example when i%n
is 5
. And we need the difference that goes into the min()
to be 0
or bigger in order for the min()
to always give us either 0
or 1
.
All modern stable browsers support abs()
, but for the best possible browser support, we can still test for support and use the fallback:
@supports not (scale: abs(-2)) {
.item { --abs: max(var(--dif), -1*(var(--dif))) }
}
Code language: CSS (css)
Also, note that if the selected column index k
is equal to n
or bigger, no items get selected.
In the interactive demo below, clicking an item selects all items on the same column:
It does this by setting --k
(in the style
attribute of the .grid
) to the index of that column.

--k
being set on the .grid
parent and used in computations on .item
childrenWe can also highlight items on either odd or even columns:
.item {
/* 1 if on an even column, 0 otherwise */
--even: min(1, mod(mod(var(--i), var(--n)), 2));
/* 1 if on an odd colum, 0 otherwise */
--odd: calc(1 - var(--even));
}
Code language: CSS (css)
This is a particular case of highlighting every k
-th column starting from column j
(again, j
is a 0
-based index and smaller than k
):
.item {
--dif: var(--j) - mod(mod(var(--i), var(--n)), var(--k));
--abs: abs(var(--dif));
--nay: min(1, var(--abs));
/* 1 if on one of every kth col starting from col of index j */
--yay: calc(1 - var(--nay));
}
Code language: CSS (css)
Highlighting items on a certain row
If we want to highlight the items on the first row, this means their index i
must be smaller than the number of columns n
. This means the difference n - i
must be bigger than 0
for items on the first row. If we clamp it to the [0, 1]
interval, we get a value that’s 0
on every row but the first and 1
on the first row.
.item {
--yay: clamp(0, var(--n) - var(--i), 1) /* 1 if on the first row! */
}
Code language: CSS (css)
There is more than one way to skin a cat however, so another approach would be to get the row index, which is the result of i/n
rounded down. If this is 0
, the item of index i
is on the first row. If it’s bigger than 0
, it isn’t. This makes the minimum between 1
and i/n
rounded down be 0
when the item of index i
is on the first row and 1
when it isn’t.
.item {
--nay: min(1, round(down, var(--i)/var(--n))); /* 1 if NOT on the first row */
--yay: calc(1 - var(--nay)); /* 1 if on the first row! */
}
Code language: CSS (css)
This second approach can be modified to allow for highlighting the items on any row of index k
as the difference between k
and i/n
rounded down (the row index) is 0
if the item of index i
is on the row of index k
and non-zero otherwise:
.item {
--dif: var(--k) - round(down, var(--i)/var(--n));
--abs: abs(var(--dif));
--nay: min(1, var(--abs)); /* 1 if NOT on row of index k */
--yay: calc(1 - var(--nay)); /* 1 if on row of index k! */
}
Code language: CSS (css)
Highlighting the items on any row includes the last one. For this, we need to know the total number t
of items on our grid. This means (t - 1)
is the index of the last grid item, and we can get the index of the row it’s on (that is, the index of the final row) by rounding down (t - 1)/n
. Then we substitute k
in the previous formula with the index of the final row we’ve just obtained this way.
.item {
/* 1 if NOT on last row */
--nay: min(1, round(down, (var(--t) - 1)/var(--n)) - round(down, var(--i)/var(--n)));
/* 1 if on last row! */
--yay: calc(1 - var(--nay));
}
Code language: CSS (css)
There are two things to note here.
One, we don’t need the absolute value here anymore, as the last row index is always going to be bigger or equal to any other row index.
Two, we’re currently passing the total number of items t
to the CSS as a custom property when generating the HTML:
- let t = 24; //- total number of items on the grid
.wrap
.grid(style=`--t: ${t}`)
- for(let i = 0; i < t; i++)
.item(style=`--i: ${i}`) #{i + 1}
But once sibling-count()
is supported cross-browser, we won’t need to do this anymore and we’ll be able to write:
.item { --t: sibling-count() }
Code language: CSS (css)
Just like before, we can highlight items on odd or even rows.
.item {
/* 1 if on an even row */
--even: min(1, mod(round(down, var(--i)/var(--n)), 2));
/* 1 if on an odd row */
--odd: calc(1 - var(--even));
}
Code language: CSS (css)
And the odd/ even scenario is a particular case of highlighting items on every k
-th row, starting from row of index j
.
.item {
--dif: var(--j) - mod(round(down, var(--i)/var(--n)), var(--k));
--abs: abs(var(--dif));
--nay: min(1, var(--abs));
/* 1 if on one of every kth row starting from row of index j */
--yay: calc(1 - var(--nay));
}
Code language: CSS (css)
Taking it Further
Another thing this technique can be used for is creating responsive grids of non-rectangular shapes with no breakpoints. An example would be the hexagon grid below. We aren’t going into the details of it here, but know it was done using this technique plus a few more computations to get the right hexagon alignment.