How to Use CSS Counters for Automated Numbering

CSS counters provide a native way to generate and control automated numbering directly within style sheets, eliminating the need to hardcode numbers in HTML or rely on JavaScript. By utilizing the counter-reset, counter-increment, and content properties alongside pseudo-elements, developers can build dynamic numbering systems for headings, ordered lists, pagination, and multi-level outlines. This article explains how these properties interact to track numerical values and render them seamlessly on a webpage.

The Mechanics of CSS Counters

CSS counters act as CSS-managed variables whose values can be initialized, tracked, and incremented based on document rules. The entire system relies on three core components:

  1. counter-reset: Creates or resets a counter variable on a specified element, typically a parent container.
  2. counter-increment: Adjusts the value of the counter (by default, adding 1) whenever a specified selector matches.
  3. counter() or counters() function: Reads the value and prints it onto the page, typically inside the content property of a ::before or ::after pseudo-element.

1. Initializing with counter-reset

The counter-reset property establishes a named counter and defines its starting value. It is usually declared on a parent element that encompasses all items that need to be counted.

.container {
  counter-reset: section-counter 0; /* Creates 'section-counter' starting at 0 */
}

If no number is specified after the name, the value defaults to 0. You can also initialize a counter to negative values or numbers other than zero.

2. Updating with counter-increment

The counter-increment property increases or decreases the counter value. This property is placed on the elements you want to count.

.section-item {
  counter-increment: section-counter 1; /* Increases 'section-counter' by 1 */
}

By default, omitting the step value increases the counter by 1. You can also specify negative values (e.g., counter-increment: section-counter -1;) to create countdowns, or steps greater than one to skip values.

3. Displaying the Counter with content

Because counters are purely stylistic mechanisms, they are rendered visually using generated content rather than changing the DOM. This is achieved using the ::before or ::after pseudo-elements.

.section-item::before {
  content: "Section " counter(section-counter) ": ";
}

The counter() function accepts two arguments: the name of the counter and an optional list style (such as decimal, upper-roman, or lower-alpha). If omitted, it defaults to standard decimal numbers.

Complete Example: Numbering Headings

Below is a practical implementation that numbers <h2> headings inside an article:

article {
  counter-reset: heading-count;
}

article h2 {
  counter-increment: heading-count;
}

article h2::before {
  content: counter(heading-count) ". ";
  color: #0066cc;
  font-weight: bold;
}

Handling Nested Numbering with counters()

For outlines, legal documents, or nested lists requiring multi-level numbering (e.g., "1.1", "1.2"), CSS provides the counters() function.

When applied to nested structures, declaring counter-reset on the parent resets the scope for each sub-level. The counters() function takes the counter name and a string delimiter used to separate the nested levels:

ol {
  counter-reset: list-counter;
  list-style-type: none;
}

li {
  counter-increment: list-counter;
}

li::before {
  content: counters(list-counter, ".") " ";
}

In this setup, a sub-item inside the second top-level item automatically displays as 2.1, then 2.2, maintaining accurate structural depth dynamically.