A classic for
loop:
for (int i = 0; i < 10; i++) {
}
Code language: JavaScript (javascript)
For most of us, some variation of this code is one of the first things we learned when we were first starting out. For me it was C++, but just about any language has some version of it—even CSS. Yes, CSS has counter variables!
The Basics
CSS Counters are driven by four properties:
counter-reset
counter-set
counter-increment
counter()
Let’s say we wanted a React component that renders a few lines of text, where the number of lines is received as a prop. But we also want to display line numbers next to each line, and we want to use CSS to do so. That last assumption might seem silly, but bear with me; we’ll look at a real-world use case at the end.
Here’s the component
const NumberedSection: FC<{ count: number }> = ({ count }) => {
return (
<div>
{Array.from({ length: count }).map((_, idx) => (
<span key={idx}>This is line</span>
))}
</div>
);
};
Code language: TypeScript (typescript)
We’ll use a CSS counter called count-val
to manage our line numbers. In CSS, we can reset our counter for each and every counter-container
<div>
.
.counter-container {
counter-reset: count-val;
}
Code language: CSS (css)
And then for each line inside that container, we can increment our counter, and render the current number in a pseudo-element.
.counter-container span::before {
counter-increment: count-val;
content: counter(count-val);
margin-right: 5px;
font-family: monospace;
}
Code language: CSS (css)
If we render two of these components like this:
<NumberedSection count={3} />
<hr />
<NumberedSection count={4} />
Code language: TypeScript (typescript)
It will display numbered lines just like we want:

If you wanted to increment by some other value than 1, you can specify whatever counter-increment
you want:
counter-increment: count-val 2;
Code language: CSS (css)
And if you wanted to just set a counter to a specific value, the counter-set
property is for you. There’s a few other options that are of course discussed on MDN.
I know this seems silly, and I know this would have been simpler to do in JavaScript. The counter variable is already right there.
A Better Use Case
Let’s get slightly more realistic. What if you have various headings on your page, representing section titles. And, as you might have guessed, you want them numbered.
Let’s start by reseting a CSS counter right at the root of our page
body {
counter-reset: tile-num;
}
Code language: CSS (css)
Then we’ll increment and display that counter for each heading that happens to be on our page. And if we want custom formatting on the line numbers, we can list out strings, and CSS will concatenate them.
h2.title::before {
counter-increment: tile-num;
content: counter(tile-num) ": ";
}
Code language: CSS (css)
Now when we have some content:
<h2 className="title">This is a title</h1>
<p>Content content content</p>
<h2 className="title">This is the next title on the page</h1>
<p>Content content content</p>
<h2 className="title">This is a title</h1>
<p>Content content content</p>
Code language: HTML, XML (xml)
We’ll get line numbers next to each heading.

One Last Example
Before going, I’d like to share the use case that led me to discover this feature. So far the examples we’ve seen are either contrived, or better served by just using JavaScript. But what if you don’t have control over the markup that’s generated on your page?
I recently moved my blog’s code syntax highlighting from Prism to Shiki. Everything went well except for one thing: Shiki does not support line numbers. This created a perfect use case for CSS counters.
I used my Shiki configuration to inject a data-linenumbers
attribute onto any pre
tag containing code I wanted numbered, and then I solved this with a little bit of CSS.
pre[data-linenumbers] code {
counter-reset: step;
}
pre[data-linenumbers] code .line::before {
content: counter(step);
counter-increment: step;
width: 1rem;
margin-right: 1rem;
display: inline-block;
text-align: right;
color: rgba(115, 138, 148, 0.4);
}
Code language: CSS (css)
Just like that, I had numbered lines

Odds & Ends
We’ve covered all you’ll probably ever use of CSS counters, but for completeness let’s look at some tricks it supports.
Formatting the numbers
It turns out you can customize the display of the number from the CSS counter. The counter()
function takes an optional second argument, detailed here.
For example, you can display these counter values as uppercase Roman numerals.
counter(tile-num, upper-roman)
Code language: CSS (css)

Nested Counters
Remember the titles we saw before? What if those containers with the numbered titles could nest within each other.
Take a look at this markup.
<div className="nested">
<h1 className="title">This is a title</h1>
<p>Content content content</p>
<h2 className="title">This is the next title</h2>
<section>
Content content content
<div className="nested">
<h3 className="title">Nested title</h3>
<p>Content content content</p>
<h3 className="title">Nested title</h3>
<section>
Content content content
<div className="nested">
<h4 className="title">Nested 2nd title</h4>
</div>
</section>
</div>
</section>
<h2 className="title">Last title</h1>
<p>Content content content</p>
</div>
Code language: HTML, XML (xml)
Do you see how those nested
containers can … nest within each other? Each new nested container resets its counter. But wouldn’t it be neat if css could take all values from the current elements’ ancestors, and connect them? Like a nested table of contents?

Well it can! Let’s take a look at the css that produced the above.
.nested {
counter-reset: nested-num;
}
.nested p {
margin-left: 10px;
}
.nested .title::before {
counter-increment: nested-num;
content: counters(nested-num, ".");
margin-right: 5px;
}
Code language: CSS (css)
To achieve this we just use the counters
function, rather than counter
. It takes a second argument that tells CSS how to join the numeric values for all counter instances on the current element. It also supports a third argument (not shown) to allow you to alter the display of these numbers, like we did before with roman numerals.
Concluding Thoughts
CSS counters are a fun feature that can occasionally come in handy. They’re a useful feature to keep in the back of your mind: they might help you out one day.