Забудь про ручную нумерацию: магия CSS-счетчиков
Представь ситуацию: дизайнер приносит макет сложного многоуровневого списка для документации или гайда. Там не просто точки или цифры, а что-то вроде «Глава 1.1», «Шаг 01:», да еще и с кастомными кружочками, градиентами и прочими радостями жизни. Ты смотришь на стандартный тег <ol> и понимаешь, что стилизовать нативный ::marker — это как пытаться припарковать фуру в узком переулке: вроде можно, но больно и неудобно.
Обычно в этот момент рука тянется к JavaScript, чтобы пробежаться циклом по элементам и проставить индексы, или, что еще хуже, ты начинаешь хардкодить цифры прямо в HTML. Остановись. Мы же не в 2010-м. Давай разберем, как заставить браузер делать всю грязную работу за нас с помощью CSS Counters.
Как мы страдали раньше
Раньше у верстальщика было два пути, и оба вели в никуда. Первый — использовать стандартный <ol> и пытаться его «причесать». Но шаг влево, шаг вправо (например, нужно вынести цифру за пределы контейнера или изменить ее формат на «01, 02, 03») — и всё ломалось.
Второй путь — «костыльный». Мы создавали пустые <span> внутри <li> и наполняли их текстом вручную. Стоило заказчику попросить поменять местами второй и третий пункты, как начинался ад с переписыванием всей нумерации. Кстати, если при такой верстке ты запутаешься в иерархии селекторов, советую освежить в памяти статью о том, как правильно работать со специфичностью каскада, чтобы стили счетчиков не конфликтовали с основными правилами.
Как делать правильно в 2026 году
CSS-счетчики — это, по сути, динамические переменные, которые живут в рамках дерева DOM. У них есть три кита: counter-reset (создаем счетчик), counter-increment (накидываем единичку) и функция counter() (выводим результат).
Современный подход позволяет делать нумерацию любой сложности, включая вложенные списки типа «1.1.2», просто используя counters() (во множественном числе). Это работает быстро, не нагружает основной поток JS и идеально подходит для создания семантичной, доступной разметки.
/* 1. Инициализируем счетчик на родительском элементе */
.custom-list {
list-style: none;
counter-reset: my-awesome-counter;
padding-left: 0;
}
/* 2. Итерируем счетчик на каждом элементе списка */
.custom-list li {
counter-increment: my-awesome-counter;
display: flex;
align-items: center;
margin-bottom: 15px;
}
/* 3. Выводим значение через псевдоэлемент ::before */
.custom-list li::before {
/* Выводим текущее значение с ведущим нулем */
content: "Шаг " counter(my-awesome-counter, decimal-leading-zero) ":";
/* Стилизуем как душе угодно */
background: #ff4e50;
color: white;
font-weight: bold;
padding: 4px 12px;
border-radius: 20px;
margin-right: 15px;
font-size: 0.85rem;
}
Для создания действительно гибких интерфейсов, где размеры элементов могут меняться, счетчики отлично сочетаются с другими современными фишками. Например, чтобы размер плашки с цифрой гармонично смотрелся на разных экранах, можно использовать отзывчивую типографику с функцией clamp().
Частая ошибка новичков
Самый частый «затык» случается, когда забывают про counter-reset. Если ты не обнулишь счетчик на родителе, то при наличии двух одинаковых списков на странице второй список продолжит нумерацию первого. В итоге вместо двух списков «1, 2, 3» ты получишь один длинный «1, 2, 3, 4, 5, 6».
Всегда вешай counter-reset на ближайший общий контейнер элементов, которые нужно пронумеровать. И помни: счетчик инкрементируется только у тех элементов, которые не скрыты через display: none. Если элемент скрыт через visibility: hidden, он все равно учитывается в нумерации. Учитывай это, когда делаешь динамические фильтры или аккордеоны.
🔥 Больше фишек, готовых сниппетов и передовых подходов к CSS мы публикуем в нашем Telegram-канале. Подписывайтесь, чтобы не пропустить!