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-resetcreates a new counter scope for child elements- Nested structures depend on properly resetting inner counters
Visibility and Rendering
display: none→ counter does not incrementvisibility: 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.