CSS Counters in Action
CSS Counters in Action êŽë š
A classic for loop:
for (int i = 0; i < 10; i++) {
}
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-resetcounter-setcounter-incrementcounter()
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>
);
};
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;
}
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;
}
If we render two of these components like this:
<NumberedSection count={3} />
<hr />
<NumberedSection count={4} />
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;
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;
}
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) ": ";
}
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>
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);
}
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)

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>
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;
}
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.