CSS counters are one of those “oh neat, didn’t know CSS could do that” features with a lot of interesting potential. In simple terms, they let you keep a running tally of things in CSS — no JavaScript needed.

Basic Counter

Here’s an easy pagination example to get us started:

See the Pen Pagination CSS Counter by Will Boyd (@lonekorean) on CodePen.

The numbers you see aren’t hardcoded in HTML. They’re generated in CSS with this:

body {
  counter-reset: pages; /* initialize counter */
}

a {
  counter-increment: pages; /* increment counter */
}

a::before {
  content: counter(pages); /* display counter */
}

Counter properties follow a “when this element is encountered in the document” flow. The body element is encountered first, initializing a counter named pages. Then the a elements are encountered, each one incrementing and displaying the pages counter.

Multiple Counters

You can have multiple counters just by using different names. This example has two overlapping counters, sections and boxes:

See the Pen Overlapping CSS Counters by Will Boyd (@lonekorean) on CodePen.

Relevant CSS:

body {
  counter-reset: sections boxes;
}

section {
  counter-increment: sections;
}

section::before {
  content: 'Section ' counter(sections);
}

.box {
  counter-increment: boxes;
}

.box::before {
  content: counter(boxes, upper-roman);
}

Here you can see the syntax for initializing multiple counters at once (line 2). And just to be extra saucy, the boxes counter is displayed as upper-roman (line 18). The full list of display options is the same as the one for list-style-type, documented here.

Counting User Selections

Now we’re getting to the fun stuff. Counter properties can be placed in pseudo-selectors like :checked. This allows counters to react to user selections via checkboxes. Here’s an example that tallies how many selections a user has made:

See the Pen Selection CSS Counter by Will Boyd (@lonekorean) on CodePen.

The CSS is really not much of a leap from our previous examples. The only differences are that we’re incrementing the counter on a pseudo-selector (input:checked) and displaying the counter just once in a dedicated .total element:

body {
  counter-reset: characters;
}

input:checked {
  counter-increment: characters;
}

.total::after {
  content: counter(characters);
}

Controlling Increments

Counters don’t have to increment by 1. They can increment by any whole number you like. They can even decrement by using negative numbers. Building on our previous example, this one sets specific increment values for each selection:

See the Pen CSS Counter Game by Will Boyd (@lonekorean) on CodePen.

The syntax is simple enough:

body {
  counter-reset: sum;
}

#a:checked { counter-increment: sum 64; }
#b:checked { counter-increment: sum 16; }
#c:checked { counter-increment: sum -32; }
#d:checked { counter-increment: sum 128; }
#e:checked { counter-increment: sum 4; }
#f:checked { counter-increment: sum -8; }

.sum::before {
  content: '= ' counter(sum);
}

While we’re on the subject, you can also control the starting value of a counter:

body {
  counter-reset: kittens 41; /* starting out with 41 kittens */
}

Potential Gotcha

An element with display: none on it will not increment a counter. If you want to hide an element but still have it contribute to a counter, you’ll have to hide it another way. Here’s one option:

input {
  position: absolute;
  left: -9999px;
}

Maybe you noticed, this is exactly what I’m doing in the last two examples. I hide the actual checkboxes for the sake of presentation, but still need them to increment the counter when checked.

Closing Remarks

Browser support for CSS counters is fantastic. Green across the board.

As awesome as CSS counters are, don’t forget about our old friends <ol> and <li>. They’re still great for a basic list of enumerated items. It’s the trickier situations that benefit from CSS counters, especially since they work on any element, giving you more freedom syntactically and semantically.

Update: I should mention accessibility. CSS counters rely on generated content in pseudo-elements. Some screen readers will pick up this content, some won’t. Because of this, it’s best not to rely on pseudo-elements for critical content. These demos were crafted to teach CSS counters in interesting ways, but I wouldn’t apply them in production as-is.