CSS Tutorial

CSS Counters Tutorial: counter-reset, counter-increment And counter() (2026-27)

By Pramod Behera  ·  Updated: June 2026  ·  12 min read
✅ In this CSS Tutorial – CSS Counters: Complete Guide to Automatic Numbering with counter-reset, counter-increment And counter()

Today we are discuss topic CSS Counters. Anywhere you see automatic numbering on the web - "Step 1," "Step 2," numbered sections in documentation, outline-style "1.1, 1.2, 2.1" headings, custom bullet numbers on a list - there's a good chance CSS Counters are doing the work, with zero JavaScript involved. CSS counters are variables maintained entirely by the browser that increment automatically as matching elements appear in your document, and stay perfectly in sync even if you add, remove, or reorder content later. In this complete guide you will master counter-reset, counter-increment, the counter() and counters() functions, multi-level nested numbering, custom numbering styles (roman numerals, letters), and real-world patterns like automatic heading numbering and step-by-step guides. Includes live code panels, an interactive counter playground, comparison tables, common mistakes, a quiz, and FAQ - everything you need to build professional, maintainable automatic numbering with pure CSS. This tutorial or document breaks down the process step by step, using simple language and real-world examples to help you master the skill.

📋 Table of Contents

  1. What Are CSS Counters?
  2. counter-reset
  3. counter-increment
  4. The counter() Function
  5. The counters() Function – Multi-Level Numbering
  6. Custom Counter Styles (Roman, Alpha, etc.)
  7. Automatic Heading & Section Numbering
  8. Step-by-Step Guides with Counters
  9. Counter Scope & Nesting Rules
  10. Counter Functions – Reference Table
  11. Best Practices
  12. Common Mistakes to Avoid
  13. Live Code Example
  14. Try It Yourself – Interactive Editor
  15. 🎨 Interactive Counter Playground
  16. Practice Quiz
  17. Frequently Asked Questions (FAQ)

✅ What Are CSS Counters?

CSS counters are variables, maintained entirely by the browser, whose value can be incremented automatically based on how many times a CSS selector matches in your document. They let you generate custom, automatic numbering - for lists, headings, figures, or any repeating element - without manually typing numbers into your HTML or relying on JavaScript.

🔄
counter-reset
Creates a counter and sets its starting value.
counter-increment
Increases the counter by 1 (or a custom amount).
🔢
counter()
Displays the counter's current value as content.
🪜
counters()
Displays nested values joined, e.g. "1.2.3".
💡 Why this matters: Because counters live in CSS, the numbering automatically stays correct even if you add, delete, or reorder items in your HTML later - there's no risk of a manually-typed "Step 3" becoming wrong after you insert a new step above it.

✅ counter-reset

counter-reset declares a named counter and sets its value - by default 0 - on the element where counting should begin (usually a parent container, like the ol or body).

/* Creates a counter named "item", starting at 0 */
ol {
  counter-reset: item;
}

/* You can also set a custom starting value */
ol.start-at-five {
  counter-reset: item 5;
}
ℹ️ "Reset" can mean "start": Despite the name, counter-reset is also how you create a counter in the first place - there's no separate "counter-create" property. Every counter must have a counter-reset somewhere before counter-increment can meaningfully count up from it.

✅ counter-increment

counter-increment increases the named counter's value - by default +1 - every time its selector matches, typically once per list item or heading.

counter-reset + counter-increment – Live Preview
ol.custom {
  list-style: none;
  counter-reset: item;
}

ol.custom li {
  counter-increment: item;
}

ol.custom li::before {
  content: counter(item) ". ";
  font-weight: 700;
  color: #0EA5E9;
}
👁 Live Output
  • First counted item
  • Second counted item
  • Third counted item

You can also increment by a custom amount, or even count downward:

li { counter-increment: item 2; }   /* +2 each time */
li { counter-increment: item -1; }  /* counts DOWN  */

✅ The counter() Function

The counter() function displays a counter's current value as generated content - it only works inside the content property of a ::before or ::after pseudo-element.

counter() with Custom Text
.steps {
  list-style: none;
  counter-reset: step;
}

.steps li {
  counter-increment: step;
}

.steps li::before {
  content: "Step " counter(step) ": ";
}
👁 Live Output
  • Open your code editor
  • Create a new HTML file
  • Link your CSS stylesheet
💡 String concatenation: Inside content, you can freely mix quoted strings and counter() calls - content: "Step " counter(step) ": "; builds a fully custom, automatically-numbered label.

✅ The counters() Function – Multi-Level Numbering

The counters() function (note the s) returns the values of a counter at every nesting level, joined by a separator string - exactly what you need for outline-style numbering like 1.1, 1.2, 2.1.

Multi-Level Nested Numbering – Live Preview
ol {
  counter-reset: item;
  list-style: none;
}
li {
  counter-increment: item;
}
li::before {
  content: counters(item, ".") " ";
  font-weight: 700;
}
👁 Live Output
  • Frontend Basics
    • HTML
    • CSS
  • Backend Basics
    • Node.js
⚠️ counter() vs counters() - easy to mix up: counter(item) on a nested item shows only that level's local value (e.g. just "2"). counters(item, ".") shows the full path through every ancestor level (e.g. "1.2") - the version you almost always want for nested outlines.

✅ Custom Counter Styles (Roman, Alpha, etc.)

Both counter() and counters() accept an optional second argument - any list-style-type keyword - to render the number in a different numbering system.

counter(item, upper-roman)
  1. First
  2. Second
  3. Third
counter(item, lower-alpha)
  1. First
  2. Second
  3. Third
li::before { content: counter(item, upper-roman) ". "; }   /* I. II. III. */
li::before { content: counter(item, lower-alpha) ") "; }   /* a) b) c) */
li::before { content: counter(item, decimal-leading-zero) " - "; } /* 01 - 02 - */

✅ Automatic Heading & Section Numbering

A very common real-world pattern: number every heading of a certain level automatically, so the numbering never gets out of sync as sections are added or reordered.

Automatic Section Numbering
.doc {
  counter-reset: section;
}

.doc h4 {
  counter-increment: section;
}

.doc h4::before {
  content: counter(section) ". ";
  color: #0EA5E9;
}
👁 Live Output

Introduction

Getting Started

Advanced Topics

Insert a new heading anywhere and the numbers automatically re-flow - no manual renumbering needed.

✅ Step-by-Step Guides with Counters

Tutorial sites, recipe pages, and onboarding flows commonly use counters to build "Step 1 / Step 2 / Step 3" badges that never drift out of sync with the actual step order.

.tutorial-steps {
  list-style: none;
  counter-reset: tut-step;
}

.tutorial-steps li {
  counter-increment: tut-step;
  position: relative;
  padding-left: 40px;
  margin-bottom: 14px;
}

.tutorial-steps li::before {
  content: counter(tut-step);
  position: absolute;
  left: 0;
  top: 0;
  width: 26px;
  height: 26px;
  background: #0EA5E9;
  color: #fff;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: bold;
  font-size: 13px;
}
ℹ️ Why this beats manual numbering: If a content editor inserts a new step between "Step 2" and "Step 3" later, hand-typed numbers would all need manual updates. A counter-based badge renumbers itself instantly and correctly, every time.

✅ Counter Scope & Nesting Rules

Each counter-reset creates a new, separate instance of that counter, scoped to the element it's declared on (and its descendants). This is what makes nested numbering and multiple independent lists work correctly on the same page.

/* Two SEPARATE counters, both named "item" */
ol.list-a { counter-reset: item; }  /* starts its own count at 0 */
ol.list-b { counter-reset: item; }  /* starts its own count at 0 too */

/* Without a counter-reset on .list-b,
   it would continue counting from .list-a instead! */
⚠️ Forgetting the reset on each scope: If you have two separate ordered lists that should each start at "1," each one needs its own counter-reset. Skipping it on the second list lets the counter keep counting up from where the first list left off.

✅ Counter Functions – Reference Table

Property / FunctionSyntax ExampleWhat It Does
counter-resetcounter-reset: item;Creates/resets a named counter, starting at 0 (or a custom value)
counter-incrementcounter-increment: item;Increases the counter by 1 (or a custom amount) per match
counter()content: counter(item);Shows the counter's value at the CURRENT nesting level only
counters()content: counters(item, ".");Shows values from ALL nesting levels, joined by a separator
Custom style argumentcounter(item, upper-roman)Renders the number using a different numbering system

✅ Best Practices

✔️ 1) Always Pair counter-reset with counter-increment
A counter that's only incremented but never reset can behave inconsistently across browsers - always declare both explicitly.

✔️ 2) Use Descriptive Counter Names
Prefer counter-reset: section; over a vague name like counter-reset: c1; - future readers (including you) will thank you.

✔️ 3) Reset Scoped Counters on Every Independent List
If multiple separate lists should each start at 1, give each one its own counter-reset rather than relying on a single page-wide counter.

✔️ 4) Prefer counters() for Any Nested/Outline Numbering
Reaching for counter() on multi-level lists is a common mistake - use counters() whenever you need the full "1.2.3" style path.

✔️ 5) Remember Counters Only Render Inside Generated Content
counter() and counters() only work inside the content property of ::before/::after - they cannot be inserted as plain text content elsewhere.

💡 Pro Tip: CSS counters work seamlessly with screen readers reading the visible generated content, but some assistive technology may not announce ::before content consistently - for critical step numbers, consider also including the number in your actual HTML text as a backup, especially for accessibility-critical instructions.

✅ Common Mistakes to Avoid

❌ Mistake 1 – Forgetting counter-reset Entirely
Using only counter-increment without a matching counter-reset can lead to inconsistent starting values across browsers - always declare the reset.
❌ Mistake 2 – Using counter() Instead of counters() for Nested Lists
counter(item) on a deeply nested item only shows that level's local number, not the full "1.2.3" path - use counters(item, ".") for true multi-level numbering.
❌ Mistake 3 – Expecting counter() to Work Outside content
counter()/counters() only function inside the content property of a ::before/::after pseudo-element - they cannot be used as a regular CSS value anywhere else.
❌ Mistake 4 – Reusing the Same Counter Name Across Unrelated Lists Without Resetting
Two separate lists sharing one counter name, with only one of them carrying a counter-reset, will cause the second list to continue counting from the first instead of starting fresh.
❌ Mistake 5 – Forgetting the Separator Argument in counters()
counters(item) without a separator string concatenates nested values with no visual break at all (e.g. "12" instead of "1.2") - always pass a separator like counters(item, ".").

✅ Complete Live Example

A documentation-style numbered outline combining multi-level counters() with custom styling:

Documentation Outline – Live Preview
.outline {
  counter-reset: sec;
  list-style: none;
}
.outline li {
  counter-increment: sec;
}
.outline li::before {
  content: counters(sec, ".");
  background: #0EA5E9;
  color: #fff;
  padding: 2px 8px;
  border-radius: 10px;
  margin-right: 8px;
}
👁 Live Output
  • 1Getting Started
  • 2Installation

✅ Try It Yourself – Interactive Editor

Edit the HTML and CSS below to experiment with CSS counters. Try switching between flat numbering, multi-level outlines, and step badges. The preview updates automatically.

🖥 Interactive CSS Counters Editor
👁 Live Preview

✅ 🎨 Interactive Counter Playground

Choose a numbering style, separator, prefix/suffix text, and starting value to instantly preview your own custom CSS counter. Copy the generated CSS with one click.

🎨 CSS Counter Playground
Generated CSS
Loading…

✅ Practice – Yes / No Quiz

1. Does counter-reset both create a new counter AND set its starting value?

2. Does counter() (without the "s") show the values from EVERY nesting level joined together?

3. Can counter() and counters() be used as a value anywhere in CSS, or only inside the content property?

4. Does CSS automatically recalculate counter values if you add or remove matching elements later?

5. Can a counter be rendered using Roman numerals or letters instead of plain numbers, via a second argument to counter()?

0/5
Your Score – Keep Practising! 🎯

✅ Frequently Asked Questions (FAQ)

What are CSS counters?
CSS counters are variables maintained by CSS that can be incremented automatically based on how many times a selector matches in a document. They let you generate custom numbering - for ordered lists, headings, figures, or any repeating element - entirely through CSS, without manually typing numbers into your HTML.
What is the difference between counter-reset and counter-increment?
counter-reset creates a counter and sets (or resets) its value, typically to 0, on a parent or container element. counter-increment increases that counter's value by 1 (or a custom amount) every time its selector matches, typically once per list item or heading. You need counter-reset to initialize the counter before counter-increment can meaningfully count up from it.
What is the difference between counter() and counters() in CSS?
counter() (no s) returns only the counter's value at the current nesting level, such as just "2" for the second item. counters() (with an s) returns the values of that counter at every nesting level, joined by a separator string, producing outline-style numbering like "1.2" or "2.1.3" for nested lists.
Can I use CSS counters to number headings automatically?
Yes. A common pattern resets a counter on the body or a wrapping section, increments it on every h2 (or similar heading), and inserts the value with content: counter(section) '. '; on the heading's ::before pseudo-element, producing automatic "1. ", "2. ", "3. " style section numbering without manually editing the HTML.
Do CSS counters work without JavaScript?
Yes, CSS counters are a pure CSS feature - no JavaScript is required. The browser automatically recalculates and re-renders counter values whenever matching elements are added, removed, or reordered in the DOM, making them ideal for numbering that should always stay in sync with the content.
Why is my CSS counter showing the wrong number or not working at all?
The most common cause is a missing or misplaced counter-reset - without it, some browsers implicitly start the counter at 0 on every matching element instead of accumulating a running total. Other common causes are forgetting the content property entirely (counters only display inside generated content like ::before/::after) or using counter() instead of counters() for multi-level nested numbering.
✍️ About the Author – Pramod Behera

Pramod Behera is the founder of LearnToSAP.com and an experienced web development educator. He creates beginner-friendly tutorials on HTML, CSS, SAP SD/MM, and frontend development, helping thousands of learners worldwide build practical skills.