Mastering CSS Counters: A Guide to Dynamic Web Numbering

Published: Updated:
3 min read 572 words
Banner

CSS counters are browser-managed variables that increment as matching elements are encountered in the document tree. This native feature enables dynamic numbering and structured labeling (e.g., headings, lists, figures) entirely through stylesheets, eliminating the need for JavaScript in many cases.


Prerequisites

  • Browser Support: Supported in all modern browsers (Chrome, Firefox, Safari, Edge)
  • Technical Knowledge: Familiarity with CSS pseudo-elements (::before, ::after)
  • Requirements: No external libraries required

Basic Implementation

1. Initialize the Counter

Define a counter using counter-reset. This establishes scope and starting value.

body {
  counter-reset: section;
}
  • Default start is 0
  • Custom start:
body {
  counter-reset: section 5;
}

2. Increment the Counter

Apply counter-increment to elements you want to track.

h2 {
  counter-increment: section;
}

3. Display the Counter

Use a pseudo-element and the counter() function:

h2::before {
  content: "Section " counter(section) ": ";
}

Complete Working Example

<body>
  <h2>Introduction</h2>
  <h2>Usage</h2>
  <h2>Conclusion</h2>
</body>
body {
  counter-reset: section;
}

h2 {
  counter-increment: section;
}

h2::before {
  content: "Section " counter(section) ": ";
}

counter() vs counters()

  • counter(name) → single-level value → 1, 2, 3

  • counters(name, separator) → nested values → 1.1, 1.2, 2.1

Use counters() when multiple counters exist across nested scopes.


Advanced: Nested Counters

For hierarchical numbering (e.g., documentation sections like 1.2.1):

body {
  counter-reset: h2;
}

h2 {
  counter-reset: h3;
  counter-increment: h2;
}

h3 {
  counter-increment: h3;
}

h2::before {
  content: counter(h2) ". ";
}

h3::before {
  content: counter(h2) "." counter(h3) " ";
}

This creates structured numbering across heading levels.


Counter Formatting

Counters support multiple numbering styles:

h2::before {
  content: counter(section, upper-roman) ". ";
}

Common formats include:

  • decimal (default)
  • lower-alpha (a, b, c)
  • upper-alpha (A, B, C)
  • lower-roman (i, ii, iii)
  • upper-roman (I, II, III)

Modern Feature: counter-set

counter-set allows updating a counter mid-flow without resetting scope:

h2.special {
  counter-set: section 10;
}

This is useful when you need to jump or sync numbering dynamically.


Common Use Cases

  • Auto-numbering headings in documentation or blogs
  • Custom ordered lists without <ol>
  • Numbering figures, tables, or code blocks
  • Step-based UI flows (e.g., onboarding steps)

Best Practices and Considerations

Accessibility

CSS-generated content is primarily presentational and may not be consistently announced by screen readers.

Recommendations:

  • Prefer semantic elements like <ol> when order is meaningful
  • Avoid relying on counters for critical information
  • Provide accessible fallbacks when necessary

Example:

<h2>
  <span class="visually-hidden">Section 1:</span>
  Introduction
</h2>

Scope and Reset Behavior

  • counter-reset creates a new counter scope for child elements
  • Nested structures depend on properly resetting inner counters

Visibility and Rendering

  • display: none → counter does not increment
  • visibility: hidden → counter does increment

Layout and Ordering Gotchas

  • Counters follow document order, not visual order
  • CSS reordering (Flexbox order, Grid) does not affect counting
  • Positioned elements (absolute, fixed) still follow DOM order

CSS counters provide a lightweight, declarative way to manage numbering and structured labels directly in CSS. When used appropriately—especially for non-critical, presentational numbering—they can simplify implementations and reduce reliance on JavaScript.

For structured documents, combining scoped counters with nested resets enables complex numbering systems with minimal overhead.

References

0
Further reading
previous
Maintaining Package Versions with pnpm and YAML Anchors
Maintaining Package Versions with pnpm and YAML Anchors
next
My Neovim Config
My Neovim Config