📝 TIL

CSS counters — 5 lines of CSS replace 20 lines of row-numbering JS

Table of Contents

Today I learned CSS counters, and deleted about 20 lines of JavaScript row-renumbering in favour of 5 lines of CSS.

I had a table where rows are added and removed client-side, and every mutation called a renumber function that walked the rows and wrote 1, 2, 3... into a cell. The CSS version:

tbody                { counter-reset: item; }    /* create the counter, at 0 */
tbody > tr           { counter-increment: item; } /* bump it once per row */
td.item-number::before { content: counter(item); } /* render it */

The number is never stored anywhere — it’s derived from document order. counter-reset instantiates the counter, counter-increment bumps it, and counter() is only readable from content, so only inside ::before / ::after.

For comparison, this is the shape of the JS it replaced — a renumber pass after every mutation:

function renumber() {
  [...tbody.children]
    .filter((tr) => !tr.classList.contains("hidden"))
    .forEach((tr, i) => (tr.querySelector(".item-number").textContent = i + 1));
}

The bigger win behind those 5 lines: the responsibility for row numbering has moved out of our application logic and into the browser. No renumber function to write, call after every mutation, or keep in sync — the numbers are derived from the document itself, so they can never drift from the actual order. Less code to maintain, and a whole class of “the numbers are wrong” bugs that no longer exist.

Side effects & edge cases

Hidden rows drop out of the count automatically

An element that generates no box (e.g. display: none) can’t increment a counter, so the rest stay contiguous. Great for soft-delete UIs. (visibility: hidden does not do this.)

Numbers aren’t in the DOM

They’re generated content — there’s nothing to assert or go stale in tests.

Browser support

Counters are CSS 2.1 — supported since IE 8, so no compatibility floor to worry about.

Try it

Play with it below: the red column is pure CSS, the green column is the old JS. Add, remove, or hide rows and they stay in sync.

Open the playground in its own tab →