CSS Counters: Auto-Number Anything Without Touching HTML
Someone reorders step 3 and step 4 in the deployment guide, and now the hardcoded numbers in the HTML are wrong. If numbers describe position, they're presentation - and presentation is CSS's job. CSS has had a full counting system since forever, and almost nobody reaches for it.
Numbered steps, zero markup
.steps {
counter-reset: step; /* create/reset a counter named "step" */
}
.steps > li {
counter-increment: step; /* +1 per item */
list-style: none;
}
.steps > li::before {
content: counter(step);
display: inline-grid;
place-items: center;
width: 1.7em; height: 1.7em;
margin-right: 0.6em;
border-radius: 50%;
background: #4f46e5;
color: #fff;
font-weight: 600;
}
Those styled number-circles every design system draws? That's the whole implementation. Reorder the list items and the numbers fix themselves.
Document-style section numbering (2.3, 2.4...)
article { 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) " "; }
Nesting resets are the key move: each h2 resets the h3 counter, so subsections restart per section. Legal documents, specs, course material - automatic.
counters() with an s: infinite nesting
For nested lists of unknown depth, the plural function joins every level with your separator:
ol { counter-reset: item; list-style: none; }
li { counter-increment: item; }
li::before { content: counters(item, ".") " "; }
/* Produces 1, 1.1, 1.1.1, 2, 2.1 ... at any depth */
Details worth knowing
- Second arguments give you formats:
counter(step, upper-roman),counter(step, lower-alpha), evendecimal-leading-zerofor that "01, 02" design-agency look. - Counter text lives in ::before content, so it isn't selectable or part of the accessibility tree the way real text is. For ordered content where the numbering carries meaning to screen readers, a real <ol> is still the right base - use counters to style it (via list-style: none + ::before) rather than replace it.
- list-item counters: you can restyle native list numbering directly via the ::marker pseudo-element when you don't need full layout control.
Support: universally available since roughly the dawn of time (IE8 had it). This is mature, boring, reliable CSS that happens to look like magic in code review.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.