Understanding CSS Counters
CSS counters represent one of the most powerful yet underutilized features in modern CSS. While traditional ordered lists (<ol>) provide automatic numbering, they offer limited control over the appearance, positioning, and styling of those numbers. CSS counters solve this by letting you treat numbers as CSS-controlled variables that can be styled independently, positioned precisely, and customized extensively. Whether you're building a step-by-step guide, a table of contents, or a documentation site, mastering CSS counters gives you complete creative control over sequential numbering without touching your HTML structure. For more advanced CSS techniques, explore our guide on CSS Modules to complement your styling toolkit.
At their core, CSS counters follow a simple lifecycle: first, a counter must be initialized (or reset) to a starting value; then, as elements are encountered, the counter's value can be incremented or decremented; finally, the current value is displayed using CSS functions. This happens entirely in the rendering engine, meaning no JavaScript is required and the counters automatically update when content changes.
Core Counter Properties
counter-reset
The counter-reset property creates a new counter and optionally sets its initial value. By default, counters start at zero, but you can specify any integer as the starting point. A single declaration can initialize multiple counters simultaneously, which is useful when managing complex nested numbering systems.
counter-increment
The counter-increment property specifies how much a counter's value should increase (or decrease, with negative values) each time an element is encountered. The default increment is 1, but you can specify any integer to create custom numbering sequences.
counter-set
The counter-set property directly sets a counter to a specific value, which is useful when you need precise control over numbering without incremental changes. Unlike counter-increment, which adds to the current value, counter-set overwrites it entirely.
1/* Initialize a counter */2body {3 counter-reset: section;4}5 6/* Increment the counter */7h2 {8 counter-increment: section;9}10 11/* Display the counter */12h2::before {13 content: "Section " counter(section) ": ";14}Displaying Counters
The counter() Function
The counter() function displays the current value of a single counter. It's perfect for flat numbering schemes where each element has its own independent number. The function accepts a counter name as its first argument and optionally accepts a counter style as a second argument.
The counters() Function for Nested Content
The counters() function is essential for nested structures where you want hierarchical numbering like "1.2.3" for third-level content. It traverses the DOM tree to collect all instances of the counter, concatenating them with a separator you define. For additional CSS selection techniques, check out our guide on case-sensitive selectors.
Counter styles available:
decimal- 1, 2, 3 (default)decimal-leading-zero- 01, 02, 03lower-alpha- a, b, cupper-alpha- A, B, Clower-roman- i, ii, iiiupper-roman- I, II, III
1/* Nested document numbering */2article {3 counter-reset: section;4}5 6article h2 {7 counter-reset: subsection;8 counter-increment: section;9}10 11article h2::before {12 content: counter(section) ". ";13}14 15article h3 {16 counter-increment: subsection;17}18 19article h3::before {20 content: counter(section) "." counter(subsection) " ";21}Custom List Number Styling
One of the most common use cases for CSS counters is replacing default list bullets with custom-styled numbers. By setting list-style: none on your list and using a pseudo-element with counter(), you gain full control over the number's appearance. This technique gives you complete creative freedom over fonts, colors, sizes, backgrounds, borders, shadows, and positioning. Complement your counter styling with CSS gradient techniques to create visually stunning numbered badges.
Styling possibilities include:
- Circular or square badges with centered numbers
- Numbers with colored backgrounds and borders
- Custom fonts, sizes, and weights
- Shadows, gradients, and hover effects
- Flexible positioning and spacing
1/* Custom numbered list */2.custom-list {3 list-style: none;4 counter-reset: item;5 padding-left: 0;6}7 8.custom-list li {9 counter-increment: item;10 position: relative;11 padding-left: 3rem;12 margin-bottom: 1rem;13}14 15.custom-list li::before {16 content: counter(item);17 position: absolute;18 left: 0;19 width: 2rem;20 height: 2rem;21 background: #3b82f6;22 color: white;23 border-radius: 50%;24 display: flex;25 align-items: center;26 justify-content: center;27 font-weight: bold;28}No JavaScript Required
Counters are handled entirely by the browser's rendering engine, making them fast and reliable without any scripting.
Full Styling Control
Style numbers independently from content using any CSS properties for backgrounds, borders, fonts, and positioning.
Automatic Updates
Numbers update automatically as content changes, eliminating manual renumbering and reducing maintenance.
Semantic HTML
Keep your HTML clean and semantic while CSS handles all visual numbering decisions.
Performance and Best Practices
Scope Counters Appropriately
Reset counters at the closest common ancestor of elements that should share the counter. This keeps numbering predictable and prevents interference between different sections. For broader CSS security considerations, see our article on CSS security vulnerabilities.
Accessibility Considerations
Screen readers don't automatically announce counter-generated numbers. Ensure semantic meaning through proper heading hierarchy, ARIA labels, or consider whether native <ol> elements would be more appropriate for content that requires screen reader support.
Browser Support
CSS counters have excellent support across all modern browsers. For older browser compatibility, consider providing fallback styling using list-style on native lists.
Performance
Counter operations occur during the layout phase, so they don't trigger repaints of the entire page. Deeply nested counters across large documents may have minor performance implications worth monitoring.
1/* Tutorial step indicators */2.step-list {3 list-style: none;4 counter-reset: step;5}6 7.step-list li {8 counter-increment: step;9 position: relative;10 padding-left: 4rem;11 min-height: 3rem;12 margin-bottom: 1.5rem;13}14 15.step-list li::before {16 content: counter(step);17 position: absolute;18 left: 0;19 top: 50%;20 transform: translateY(-50%);21 width: 2.5rem;22 height: 2.5rem;23 background: linear-gradient(135deg, #667eea, #764ba2);24 color: white;25 border-radius: 12px;26 display: grid;27 place-items: center;28 font-weight: bold;29 font-size: 1.1rem;30 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);31}