Get the number of auto-fit/auto-fill columns in CSS

Ana Tudor Ana Tudor on

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:

base_idea_fallback
the result in non-supporting browsers: no number of columns can be computed

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.

A diagram illustrating basic trigonometry, featuring the tangent function, labeled 'tan(a) = opposing / adjacent', with definitions for 'opposing cathetus' and 'adjacent cathetus', and an angle 'a' represented in a right triangle.
basic trigonometry recap

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?

A mathematical diagram illustrating the relationship between the tangent function, the angle 'a', and the lengths 'w' and 'u' in a right triangle.
using trigonometric functions to get around browser support gaps

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.

Screenshot of browser developer tools showing a grid layout with items numbered from 1 to 18. The grid has a header indicating the number of auto-fill columns, which is 8. The inspector highlights CSS variables related to grid sizing.
rounding error caught by Firefox DevTools

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.

Mind the gap

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.

A code snippet from a web developer's browser console showcasing CSS rules for items in a grid layout, including custom properties for styling.
Chrome DevTools screenshot showing --k being set on the .grid parent and used in computations on .item children

We 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.

Wanna learn modern CSS layout?

Frontend Masters logo

Laying out designs on the web with CSS has gotten a lot more powerful in recent years. CSS grid, flexbox, and container queries are incredibly powerful tools for that, and we have a complete learning course on them from Jen Kramer.

7-Day Free Trial

Leave a Reply

Your email address will not be published. Required fields are marked *

$871,946

Frontend Masters donates to open source projects through thanks.dev and Open Collective, as well as donates to non-profits like The Last Mile, Annie Canons, and Vets Who Code.