Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

CSS Counters: Auto-Number Anything Without Touching HTML

C
Carlo Fontanos
· 2 min read

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), even decimal-leading-zero for 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.

C
Written by Carlo Fontanos

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.

Message sent!

Your details are only used to reply to you.

Keep reading