Using CSS Counters

Master automatic numbering without JavaScript using CSS counter-reset, counter-increment, and counter() functions for headings, code blocks, and documentation.

What Are CSS Counters?

CSS counters are a powerful native CSS feature that enables automatic numbering and counting without requiring JavaScript. Think of them as CSS variables specifically designed for incrementing and displaying numerical values throughout your document. Whether you need to automatically number headings in documentation, create stepped process indicators, or add line numbers to code blocks, CSS counters provide an elegant solution that is fully declarative and performant MDN Web Docs.

Unlike ordered lists which are limited to <li> elements, CSS counters can be applied to any HTML element, giving you complete flexibility over where and how numbering appears. This capability has been supported across all modern browsers for years, making it a reliable technique for production websites.

For professional web development services that leverage modern CSS techniques, understanding counters is essential for creating maintainable, well-structured documentation sites.

Practical Applications

CSS counters excel in scenarios where automatic numbering based on document structure is needed. Consider a documentation site where each heading needs sequential numbering--counters handle this automatically as you add or reorganize content. Code examples benefit from line numbering that can be added purely through CSS without modifying the underlying syntax highlighter. Form wizards and step processes use counters to indicate progress, updating dynamically as users move through the workflow. The key insight is that whenever you need automatic numbering based on document structure, CSS counters provide the cleanest solution without any JavaScript overhead Frontend Masters.

This three-step process forms the foundation for all CSS counter implementations: initialize with counter-reset, advance with counter-increment, and display with counter(). Understanding this flow is essential because counters are scoped to elements--they don't exist globally across the entire page but are instead created and managed within specific DOM subtrees Samantha Ming.

Why Use CSS Counters?

Key advantages of native CSS numbering

No JavaScript Required

Counters are handled entirely by the browser's CSS rendering engine, requiring no runtime calculations or script execution.

Zero Bundle Size Impact

Add comprehensive numbering to your site without increasing JavaScript bundle weight--perfect for performance optimization.

Server-Side Compatible

Works seamlessly with static site generators, SSR, and SSG--no client-side hydration required for numbering.

Universal Browser Support

Supported across all modern browsers including Internet Explorer 8+, making it a reliable production technique.

Core Concepts: The Three Pillars

CSS counters operate through three fundamental operations that work together to create automatic numbering throughout your document.

1. Initialization with counter-reset

The counter-reset property creates a new counter and establishes its scope within your document. This is your starting point for any CSS counter implementation. The counter name must be a valid CSS identifier, and you can optionally set an initial value. By default, when you create a counter without specifying a value, it starts at zero. You can reset multiple counters in a single declaration, which is useful when several independent counters operate within the same scope MDN Web Docs.

/* Create a counter named 'section' starting at 0 */
counter-reset: section;

/* Create a counter starting at 1 */
counter-reset: step 1;

/* Reset multiple counters in one declaration */
counter-reset: chapter 1 section 0 item 0;

2. Incrementation with counter-increment

The counter-increment property controls how and when counter values advance. By default, incrementing a counter adds one to its value, but you can specify any integer to create different counting patterns. Positive integers count upward while negative integers create countdown effects. The increment typically occurs on elements that should trigger the counter to advance, such as headings in a section or steps in a process Samantha Ming.

/* Default increment by 1 */
counter-increment: section;

/* Increment by 2 */
counter-increment: section 2;

/* Decrement (countdown) */
counter-increment: countdown -1;

3. Display with counter() and counters()

The counter() function retrieves and displays the current value of a single-level counter, taking the counter name as its primary argument and optionally accepting a counter style. The counters() function (plural) enables hierarchical numbering by retrieving values from all ancestor counter scopes, with a separator string between values. This distinction is crucial for choosing the right function based on your numbering needs MDN Web Docs counters().

/* Basic display */
content: counter(section);

/* With label */
content: "Section " counter(section) ": ";

/* Nested counters with dot separator */
content: counters(section, ".") ". ";

The Scope and Inheritance Model

When you create a counter using counter-reset, that counter exists within the scope of the element where it was reset and all of its descendants. Child elements can increment and display this counter, and if they reset the same counter name, they create a nested instance that operates independently. This means nested counters with the same name don't interfere with each other--each scope maintains its own counter instance MDN Web Docs.

Basic Example: Numbered Headings

Here's a complete implementation for numbering documentation headings. The key to success is placing the counter-reset on an ancestor element that contains all elements you want to count, then incrementing and displaying on the child elements themselves.

/* Reset counter at the container level */
.docs-page {
 counter-reset: heading;
}

/* Increment and display on h2 elements */
.docs-page h2::before {
 counter-increment: heading;
 content: counter(heading) ". ";
 margin-right: 0.5rem;
 color: var(--primary-color);
}

HTML Structure:

<article class="docs-page">
 <h2>Introduction</h2>
 <p>Content goes here...</p>
 
 <h2>Getting Started</h2>
 <p>More content...</p>
 
 <h2>Advanced Topics</h2>
 <p>Even more content...</p>
</article>

Result:

  1. Introduction
  2. Getting Started
  3. Advanced Topics

Why Placement Matters

A common mistake developers make is placing the counter-reset on the same element where they increment. If you reset and increment on h2, each heading starts fresh rather than continuing from a shared sequence. The counter reset must be placed on an ancestor element that contains all the elements you want to count. For reusable components that might appear multiple times on a single page, place counter-reset on the component root to ensure each instance maintains its own independent counter scope Samantha Ming.

Nested Counters for Hierarchical Numbering

Nested counters enable hierarchical numbering schemes like "1.2.3" or "Section 2 > Subsection 1". This uses the counters() function (plural) which retrieves values from all ancestor counter scopes. Each time you reset a counter within a nested scope, that reset creates a new counter instance while still allowing access to all ancestor instances MDN Web Docs counters().

When to Use counters() vs counter()

Use counter() for single-level counting where you only need the current scope's value. Use counters() when you need hierarchical numbering that includes ancestor counter values. The counters() function takes a separator string as its second argument, which is placed between counter values from different nesting levels.

/* Single-level: shows just the current number */
content: counter(section); /* Output: 1, 2, 3... */

/* Hierarchical: shows complete path */
content: counters(section, "."); /* Output: 1.2.1, 1.2.2... */
content: counters(heading, " > "); /* Output: 1 > 2 > 1... */

Complete Hierarchical Example

.section {
 counter-reset: chapter;
}

.section h2 {
 counter-reset: section;
}

.section h2::before {
 counter-increment: chapter;
 content: counter(chapter) ". ";
}

.section h3 {
 counter-reset: subsection;
}

.section h3::before {
 counter-increment: section;
 content: counter(chapter) "." counter(section) ". ";
}

/* For h4 and deeper, use counters() for full hierarchy */
.section h4::before {
 counter-increment: subsection;
 content: counters(chapter, ".") "." counter(subsection) " ";
}

Output:

  1. Introduction 1.1. Background 1.1.1. History 1.1.2. Overview 1.2. Main Content
  2. Conclusion

The beauty of this system is that it automatically adapts to your HTML nesting depth. Whether you have two levels of nesting or five, the counters() function will display the complete hierarchical path without requiring explicit tracking of which level you're at Frontend Masters.

CSS Counter Style Options
StyleOutputUse Case
decimal1, 2, 3...Default numbering
decimal-leading-zero01, 02, 03...Aligned numbers
lower-alphaa, b, c...Subsections
upper-alphaA, B, C...Formal outlines
lower-romani, ii, iii...Classic numbering
upper-romanI, II, III...Chapters, volumes
lower-greekα, β, γ...Mathematical works
thai๑, ๒, ๓...Thai language
symbols*, †, ‡...Footnotes, awards

Real-World Use Cases

1. Code Block Line Numbers

Many syntax highlighters don't include line numbers. CSS counters solve this elegantly without requiring additional JavaScript libraries or modifying the underlying code structure. The counter increments for each line span, creating a visual line number column Frontend Masters.

pre[data-line-numbers] {
 counter-reset: line;
}

pre[data-line-numbers] code {
 counter-reset: line;
}

pre[data-line-numbers] .line::before {
 counter-increment: line;
 content: counter(line);
 display: inline-block;
 width: 2.5em;
 margin-right: 1em;
 text-align: right;
 color: rgba(115, 138, 148, 0.5);
 user-select: none;
}

2. Step Process Indicators

Form wizards and multi-step processes benefit from visual step indicators that track progress through the workflow. CSS counters create these indicators without JavaScript, and they update automatically as steps are added or removed.

.process-steps {
 counter-reset: step;
}

.step-item {
 position: relative;
 padding-left: 3rem;
}

.step-item::before {
 counter-increment: step;
 content: counter(step);
 width: 2rem;
 height: 2rem;
 border-radius: 50%;
 background: var(--primary-color);
 color: white;
 display: flex;
 align-items: center;
 justify-content: center;
 position: absolute;
 left: 0;
 top: 0;
}

3. Figure and Table Captions

Documentation and editorial content often requires numbered figures and tables. CSS counters provide a clean separation between content and presentation, making it easy to update figure numbering across an entire site.

.figure-container {
 counter-reset: fig;
}

.figure-container figure {
 counter-increment: fig;
}

.figure-container figcaption::before {
 content: "Figure " counter(fig) ": ";
 font-weight: bold;
}

4. Table of Contents Indicators

Long-form content benefits from inline TOC references that show current position. Combine counters with CSS custom properties to create dynamic section indicators that update as users scroll through content.

5. Reversed Counters for Countdown Effects

Reversed counters start from the number of elements and count down, useful for "remaining items" indicators or reverse-ordered sequences. The browser automatically calculates the starting value based on element count.

/* Reversed counter for countdown effect */
.countdown-list {
 counter-reset: reversed(item);
}

.countdown-list li::before {
 counter-increment: reversed(item);
 content: counter(item);
}

For advanced web development techniques, CSS counters are an essential tool in your CSS arsenal for creating professional documentation and numbered content without JavaScript overhead.

Frequently Asked Questions

Best Practices

Do: Reset at the Right Scope

Place counter-reset on a container that encompasses exactly the elements you want to count. Avoid resetting on too-high-level elements like body if only a specific section needs counting. Place it as close to the counting scope as possible to reduce the scope of counter operations and improve rendering performance Frontend Masters.

Do: Use Descriptive Counter Names

Instead of generic names like "counter", use specific names like "figure", "step", "chapter", or "section". This prevents conflicts and makes your CSS more maintainable, especially when multiple counters operate in overlapping scopes.

Don't: Reset and Increment on the Same Element

If you reset and increment on the same element (like h2), each heading starts fresh rather than continuing from a shared sequence. Place counter-reset on the parent container and counter-increment on the child elements to maintain proper sequencing.

Do: Test with Dynamic Content

Verify your counters work correctly when elements are added or removed dynamically. Counters should update automatically, but complex nested structures may need testing across different scenarios to ensure consistent behavior.

Consider: Accessibility

Counter-generated content in pseudo-elements is not always announced by screen readers. For critical numbered content that must be accessible, consider adding visible numbers in the HTML itself or providing fallback content for assistive technologies.

Performance Considerations

CSS counters are highly performant because they operate entirely within the CSS rendering pipeline without JavaScript execution. However, deeply nested counter structures with counters() produce longer strings, so consider your nesting depth. For Next.js applications, this means you can add comprehensive heading numbers, process step markers, or table of contents indicators without adding any JavaScript bundle weight--perfect for performance optimization.

Well-structured documentation with automatic numbering also contributes to search engine optimization, as clear hierarchies and navigation help search engines understand and index your content more effectively.

Browser Compatibility

CSS counters are supported in:

  • All modern browsers: Chrome, Firefox, Safari, Edge
  • Internet Explorer: Version 8 and above
  • Mobile browsers: iOS Safari, Chrome for Android

According to Can I Use, CSS counters have excellent cross-browser support with no vendor prefixes required for current browser versions. This makes them a reliable choice for production websites targeting a wide range of users.

Common Pitfalls to Avoid

  1. Forgetting the content property: Counter values only appear when used in the content property of pseudo-elements
  2. Incorrect scope placement: Placing reset too high or too low breaks the counting logic
  3. Confusing counter() with counters(): Using the wrong function for your hierarchical needs
  4. Over-nesting: Deeply nested structures can produce confusing numbered output
  5. Not testing with real content: Counter behavior may vary with dynamic content loading

Ready to Build Better Web Experiences?

Our team specializes in modern web development using Next.js and CSS best practices. Let's discuss how we can help your project with performance-optimized solutions.