CSS Counters: auto-update list numbers without JavaScript

This is a quick-tip about a CSS feature. It is the kind of feature most developer (and LLM’s) would use JavaScript for. With CSS you get this for free. Knowing about it, makes you a better developer! 🏆

Recently I helped build a new product that involved pages with content in a specific order.

For the ordering I used the nice positioning gem. This gives sequentially numbering (1, 2, 3 and so on).

So you can display them like this:

The ERB is something simple like this:

<%# locals: (page:) %>
<li draggable data-reposition-id-value="<%= page.id %>">
  <%= tag.span page.position class: "" %>

  <%= tag.p page.content %>
</li>

(reposition is coming from the Kanban article)

But, of course, the pages needed to be sortable. So I used the code from the article Create a Kanban board with Rails and Hotwire (a routing concern, Stimulus- and Rails controller and some additions to the HTML 😎) and, yay, in no-time, pages could be sorted in a custom way:

But wait, the numbers are now incorrect. I can refresh the page, or maybe used morph and the numbers will match up again, but this is of course not acceptable.

So how’d you do this? If I hadn’t already hinted at CSS above, would you reach for JavaScript? Extend the reposition Stimulus controller? Or add a dedicated controller for this purpose? It would likely be only a 10-line controller, so why not?

Because CSS has a feature that can help you with this: CSS counters.

CSS counters automatically track and display numbers based on the DOM order. Not the database order. Add this to the wrapping ul element [counter-reset:page-num] and update the page partial like this:

<%# locals: (page:) %>
<li draggable data-reposition-id-value="<%= page.id %>">
  <%= tag.span class: "before:content-[counter(page-num)] [counter-increment:page-num]" %>

  <%= tag.p page.content %>
</li>

If you use realvanilla CSS, you can use:

ul { counter-reset: page-num; }

li span::before {
  content: counter(page-num);
  counter-increment: page-num;
}

So how do CSS counters work? You initialize a counter with counter-reset on a parent element. Then each time you use counter-increment, it bumps up by one. Finally, counter() displays the current value. The browser handles all the counting automatically as elements move around in the DOM.

Why not use ol>li and style the ::marker pseudo-selector? Great question! The ::marker pseudo-element is quite limited in what you can style. You can change colors, fonts and the content itself. But that’s about it. No backgrounds, borders, padding or positioning. If you need more control over the visual styling of your numbers, like adding backgrounds, custom spacing or complex layouts (like I needed in above product, but not showed), you’ll need to use CSS counters with regular elements instead.

And that is it. Yet another powerful CSS feature you can use instead of JavaScript! ❤️

Product-minded Rails notes

Once a month: straightforward notes on improving UX in Rails—what to simplify, what to measure, and UI/frontend changes that move real usage.

Over to you…

What did you like about this article? Learned something knew? Found something is missing or even broken? 🫣 Let me (and others) know!

Comments are powered by Chirp Form

Want to read me more?