CSS counters are a built-in way to generate automatic numbering in your UI—without writing numbers in your HTML.
They’re perfect for things like step-by-step components, numbered headings, figure labels, and fancy lists.
The key idea: you define a counter, you reset it where you want numbering to start, and you increment it on each item.
Then you print the number with content.
This grid is numbered by a CSS counter. The numbers do not exist in the HTML.
Click the snippets to switch how the number is printed.
Install
Get the thing, put it in place, pretend you meant to do it earlier.
Configure
Change settings until it “feels right” (a valid engineering technique).
Ship
Push to production and immediately watch the logs like a hawk.
Celebrate
Optional, but recommended. Even small wins count.
Iterate
Improve the parts you now realize you should’ve done yesterday.
Document
Future-you will be suspiciously grateful.
Here’s what happened:
The parent (.list) resets a counter named card to 0.
Each .card::before increments the counter (so the first card becomes 1).
The content property prints the current value with counter(card).
Starting values and step sizes
Counters don’t have to start at 1, and they don’t have to increment by 1.
You can set a start value in counter-reset and a step size in counter-increment.
This is the classic “section + subsection” setup. Each section increments section and resets sub.
Sub-items increment sub. Try the third snippet to see counters() in action.
Getting started
Install
Put the files where they need to go.
Run
Start it up and make sure it actually works.
Configuration
Defaults
Choose sensible defaults so users don’t hate you.
Overrides
Make it customizable without making it fragile.
The superpower pattern: reset inside the item
Notice this line:
counter-reset: sub; inside .section
That’s the “nested numbering” trick. Each time a new section appears, it resets the subsection counter back to 0,
so the first subsection in each section becomes 1 again.
Real-world pattern: numbered headings
Let’s turn a list of headings into an auto-numbered mini article.
This is especially nice for long tutorials or documentation pages.
A wide banner image with plenty of room for text.A second image that still feels consistent in the layout.Changing the order of figures automatically updates numbering.Perfect for tutorials, docs, and “as seen in Figure X”.
Custom list numbering with counters
Ordered lists (<ol>) already have numbering, but counters let you style and format that numbering
in ways
default list markers can’t. You can also use counters on an unordered list to create “numbered bullets.”
If the order is meaningful (like “do these steps in sequence”), use a real <ol> in HTML.
CSS counters are still useful for styling, but you want the semantic meaning of an ordered list.
Styling counters like badges and timelines
Counters really shine when you combine them with layout and pseudo-elements.
Let’s build a mini “timeline” look: a line on the left and numbered nodes.
Write the first version without judging it too hard.
Review
Fix the confusing parts and remove the “oops” moments.
Publish
Make it real. Then iterate based on feedback.
Maintain
Small improvements over time beat massive rewrites.
Common mistakes: “Why isn’t my CSS counter working?”
You forgot to reset the counter.
If the parent never runs counter-reset, the counter might not exist (or might be inherited from
somewhere unexpected).
Add counter-reset: name; on a clear container.
You incremented the wrong element.
The element with counter-increment is the one that “counts”.
If you put it on the container instead of each item, you’ll only increment once.
Your pseudo-element doesn’t render.
If you’re using ::before or ::after, it must have content (even if it’s
empty).
In counter examples, you almost always want content: counter(name);.
You reset too often.
If you put counter-reset on each item, the counter restarts every time—so every item becomes “1”.
Reset on the parent, increment on the children.
Nesting seems “off”.
For nested numbering, remember the pattern: the parent increments its own counter and resets the child counter.
That’s how you get “2.1, 2.2” inside section 2.
Accessibility and best practices
CSS counters usually appear via generated content (::before / ::after).
That’s great visually, but it’s not always guaranteed to be announced the same way by assistive tech.
When order matters, use real HTML structure.
For step-by-step instructions, prefer an <ol> and style it. Counters can enhance, but semantics
should come first.
Don’t rely on the number alone.
Pair numbers with clear headings or labels like “Step”, “Section”, “Figure”, so meaning doesn’t disappear if
styles fail.
Keep counters predictable.
Reset on obvious containers, name your counters clearly (step, figure,
section), and avoid “mystery inheritance”.
Quick cheat sheet: CSS counters
Create/reset:counter-reset: name; or counter-reset: name 0;
Increment:counter-increment: name; or counter-increment: name 1;
Print:content: counter(name);
Nested print:content: counter(a) "." counter(b);
Deep nesting chain:content: counters(name, ".");
CSS counters conclusion
CSS counters are one of those features that feel like a party trick… until you use them in real components and never
want to manually type “Step 17” again.
Start simple: reset on the parent, increment on the items, print with content.
Then level up with nested counters and richer styling.