Stop Manually Numbering Your Lists Like It is 1999
Picture this: you are building a complex documentation page or a fancy "How-To" section. The designer wants big, bold numbers that overlap the text, have gradients, and maybe even a custom suffix like "Step 01". If you reach for a standard <ol> and <li>, you are going to hit a wall fast. The default styling for list markers is notoriously stubborn and limited. You can change the type to "decimal" or "roman," but making them truly "pop" usually leads to a world of pain. That is exactly where CSS counters come in—a way to turn any element into a smart, auto-incrementing machine without touching a single line of JavaScript.
How we suffered before
Back in the day, if we needed custom-styled numbers, we usually resorted to some pretty ugly hacks. The most common "solution" was hardcoding the numbers directly into the HTML: <span class="number">1.</span>. It worked until the client decided to add a new step in the middle of the list. Suddenly, you were manually re-numbering twenty items like a digital scribe from the Middle Ages.
Some of us tried to get clever with JavaScript, looping through DOM elements and injecting index values. While it worked, it felt like overkill for something that should be handled by the layout engine. It also made the page flicker on load if the script ran too late. When styling these hacks, we often ran into issues with positioning and layout shifts. If you've ever struggled with getting these manual markers to align perfectly with your text, you might want to check out our guide on How to Properly Work with Cascade Specificity to see why your "quick fixes" sometimes get ignored by the browser.
The modern way: CSS Counters in 2026
The modern approach is elegant, performant, and purely declarative. CSS counters allow us to create variables that the browser increments automatically as it renders the page. There are three main components to this magic: counter-reset, counter-increment, and the counter() function used within the content property.
The beauty of this system is that it works on any element—not just list items. You can number headings, divs, or even checkboxes. If you are building a highly responsive layout and want your numbers to scale perfectly with your headers, consider pairing counters with responsive (fluid) typography with the clamp() function. This ensures your custom counters look just as sharp on a mobile screen as they do on a 27-inch monitor.
Ready-to-use code snippet
Here is a clean, modern implementation of a custom-styled list using CSS counters. This example creates a sleek "Step" counter with custom styling that you simply cannot achieve with standard list markers.
/* 1. Initialize the counter on the parent container */
.custom-list {
counter-reset: step-counter;
list-style: none;
padding: 0;
}
/* 2. Increment the counter on each child item */
.custom-list li {
counter-increment: step-counter;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 15px;
font-family: sans-serif;
}
/* 3. Display the counter using a pseudo-element */
.custom-list li::before {
/* This is where the magic happens */
content: "Step " counter(step-counter, decimal-leading-zero);
/* Modern styling that standard markers can't do */
background: linear-gradient(45deg, #6366f1, #a855f7);
color: white;
padding: 5px 12px;
border-radius: 8px;
font-weight: bold;
font-size: 0.8rem;
text-transform: uppercase;
}
Common beginner mistake
The biggest "gotcha" for developers new to CSS counters is forgetting the counter-reset. If you don't initialize the counter on a parent container, the browser doesn't know where the count starts. In many cases, if you skip the reset, the counter will continue globally across your entire page. You might end up with "Step 45" on your second list because the browser is still counting items from the first list.
Another common mistake is placing counter-increment on the ::before pseudo-element itself rather than the list item. While it might appear to work in some browsers, it's semantically incorrect and can lead to weird bugs if the pseudo-element is hidden or rendered multiple times. Keep the logic on the actual element and the display on the pseudo-element. Keep it clean, and your future self (and your teammates) will thank you!
🔥 We publish more advanced CSS tricks, ready-to-use snippets, and tutorials in our Telegram channel. Subscribe so you don't miss out!
Top comments (0)