Not a Number

Subscribe
Archives
November 27, 2021

A Warm Welcome to CSS Counters

Hi there!

Thank you so much for subscribing to the Not a Number newsletter! I made it a new years resolution to start a newsletter, and while it has taken me some time to get one running, I can’t express how grateful I am to you for helping me make this a reality.

I’m still experimenting on what exactly I want these newsletters to be, but for now I’m imagining these letters to be “mini” versions of my blog posts — ideas that don’t quite have the scope of a full post but are otherwise still fun to know about.

Anyways, enough rambling. On to the first letter!

CSS-Only Counters

The other day I was peeking at Josh Comeau's blog to figure out how he styled his numbered lists when I found this weird CSS property called counter-increment:

CleanShot 2021-11-22 at 11.10.24@2x.png

The wrapping ol element also has a seemingly related property called counter-reset:

CleanShot 2021-11-25 at 15.59.13@2x.png

Huh. It seems to imply that CSS has a built-in way of manipulating counters—I've never heard of this before! So what are these properties, and how do they work? Let's take a look.

Sidenote: Josh has a sale on his CSS for JS devs course going on right now — it's probably the best course on CSS I've ever taken. I'd recommend picking up a copy if you're able, even if you feel like you know CSS well.


After a bit of research, I found that my hunch was correct—these properties belong to a feature in CSS called CSS counters. CSS counters are exactly what they sound like — a way to maintain and manipulate a counter using pure CSS properties, which you can then use to style elements based on the counter value.

Josh here used it to style a numbered list, but after some experimenting, I found that you can also use it to display the number of items in a list without any JavaScript:

CleanShot 2021-11-13 at 21.25.12.gif

(Although some JavaScript was involved in making the add button work).

So how does it work? Here's the styles used in the above demo:

ul {
    counter-reset: list;
}

li {
    counter-increment: list;
}

#counter-value:before {
    content: "List items: " counter(list);
}

As you can see, there are two properties used to set up and change the value of the counter: counter-reset and counter-increment respectively. Then, the function counter(<name>) is used to read the value of the counter with the given name.

In the list example above, we set up a counter called list on every ul element by using the counter-reset property:

ul {
    counter-reset: list;
}

Then, we increment the value of the list counter by 1 for every li element within the ul element:

li {
    counter-increment: list;
}

So given the following markup, our counter values would be:

<ul> <!-- sets up counter; value = 0 -->
    <li>One</li> <!-- value = 1 -->
    <li>Two</li> <!-- value = 2 -->
    <li>Three</li> <!-- value = 3 -->
    <li>Four</li> <!-- value = 4 -->
</ul>

Finally, we read the value of the list counter by using the counter() function:

#counter-value:before {
    content: "List items: " counter(list);
}

Which gives us something like:

CleanShot 2021-11-25 at 16.06.46@2x.png

We’re using a :before pseudo-element here because the only way (that I know of) to modify the contents of an element is to use the content property, which only works in a pseudo-element like :before.

Cool! So to summarize:

  1. Set up a counter using counter-reset, providing the name of the counter as an argument
  2. Increment the value of the counter using counter-increment
  3. Read the value of the counter using the counter() function

Multiple Counters

One thing you might be thinking is, what happens if we nest counter elements? Wouldn’t we end up with multiple counters with the same name? Would the new counters override the previous ones?

Take the following snippet for example:

<ul> <!-- list counter #1 -->
    <li>One</li>
    <li>
        Two
        <ul> <!-- list counter #2 -->
            <li>Nested one</li>
        </ul>
    </li>
    <li>Three</li>
</ul>

Each ul element initializes a counter called list, and each li simultaneously increments and reads the counter value:

li {
    counter-increment: list;
}

li:before {
    content: "Counter value: " counter(list);
}

What gets printed out in each li element?

CleanShot 2021-11-25 at 16.09.07@2x.png

As you can see, the inner li prints 1 as expected, and the last li in the outer list prints 3. So it looks like the inner ul resets the counter, but somehow doesn’t override the other counter that already exists.

It turns out that nesting counters creates separate counter instances. Each instance shares the same name but have individual values. When you try to use the value of a counter using counter-increment or counter(), you use the counter that was made by the closest parent. In the inner li, that counter is the one set by the nested ul; in the outer li, that counter comes from the outer ul.

<ul> <!-- list counter #1 -->
    <li>One</li>
    <li>
        Two
        <ul> <!-- list counter #2 -->
            <li>Nested one</li> <!-- reads from counter 2 -->
        </ul>
    </li>
    <li>Three</li> <!-- reads from counter 1 -->
</ul>

You can also choose to read the values of all available counters through the plural counters() function. This function works just like the singular counter(), except it also takes in a separator string as the second argument of the function:

li {
    counter-increment: list;
}

li:before {
    content: "Counter value: " counters(list, "."); /* separate values of list instances with "." */
}

Now instead of showing 1, the inner list will show 2.1 because the value of the outer counter is 2 and the value of the inner counter is 1:

CleanShot 2021-11-25 at 16.10.25@2x.png

Summary

And that’s all I have on CSS counters! Hope you learn a thing or two about this pretty obscure CSS property — I definitely did. There's a lot more that I want to talk about when it comes to counters, but this letter is getting a bit too long as is.

Anyways, thanks again for subscribing and let me know your thoughts! Do you like this format? Or would you prefer something more concise with links to the main website? What would you like to see in this letter? You can always reply to this email and it will be sent directly to my inbox.

Thanks again,

Nanda

Don't miss what's next. Subscribe to Not a Number:
This email brought to you by Buttondown, the easiest way to start and grow your newsletter.