Знакомство sibling-index() и sibling-count()
В этой демонстрации мы познакомимся с функциями sibling-index() и sibling-count(). На данный момент они доступны в браузере Chrome, поэтому лучше просматривать демонстрацию именно в нём.
В качестве примера используем вращающийся лоадер. Его лепестки отличаются друг от друга значением одного или нескольких свойств.
Видеоверсия этого курса доступна во ВКонтакте и на Ютюбе
Подробный разбор лоадера доступен в этой демонстрации. Здесь мы сфокусируемся на стилизации лепестков.
Сначала рассмотрим «олдскульный» способ стилизации, при котором стили для каждого лепестка задаются вручную.
В этом спиннере у каждого лепестка есть свой угол поворота и своя задержка анимации. Устанавливаем значения для первого лепестка с помощью псевдокласса :nth-child(1).
Зададим значения для второго лепестка с помощью псевдокласса :nth-child(2).
Зададим значения для третьего лепестка с помощью псевдокласса :nth-child(3).
Уже сейчас можно заметить, что значения изменяются по линейной зависимости. Поэтому можно создать CSS-правила для всех 12 лепестков с помощью цикла в препроцессоре.
В конечном счёте в стилях появится «батарея» CSS-правил. Причём не важно, написаны правила вручную или сгенерированы с помощью препроцессора. В браузер пользователя отправляются именно эти стили.
Это старый способ, который использовался до появления CSS-переменных и функции calc().
Сейчас (осень 2025 года) популярным стал второй способ решения этой задачи, который позволяет значительно сократить объём стилей. Однако за это приходится платить «засорением» разметки.
Добавим в разметку каждого элемента атрибут style с переменной --i. Зададим для --i значения от 1 до 12.
Зададим трансформацию поворота через формулу calc(30deg * var(--i)). То есть каждый элемент поворачивается на дополнительные 30 градусов.
Удалим свойства transform: rotate(x) из отдельных правил. Ничего не сломалось.
Зададим задержку анимации с помощью формулы calc(var(--i) * 0.1s - 1.2s). То есть задержка каждого элемента изменяется на 0.1s относительно предыдущего, начиная с -1.1s у первого элемента и заканчивая 0s у последнего. Удалим оставшиеся правила с использованием :nth-child.
Во втором способе «батарея» правил заменена выражением с calc() и переменной-индексом. Но есть минус — засорение разметки.
У третьего способа нет и этого минуса. Давайте познакомимся с ним.
Сначала проведём небольшой рефакторинг. Разобьём составное свойство animation на отдельные CSS-свойства для улучшения читаемости.
Добавим в начало CSS-правила .loader__stick { ... } переменную --item-delay и сохраним в неё значение задержки.
По аналогии добавим переменную --item-angle, в которую сохраним угол поворота элемента.
Рефакторинг завершён. Все важные вычисления расположены в самом верху CSS-правила. Мы можем сосредоточиться на них и не обращать внимания на CSS-свойства ниже.
А теперь магия. Заменим в обеих формулах var(--i) на функцию sibling-index(), которая возвращает порядковый номер элемента.
Если вы используете последнюю версию Chrome, то лоадер продолжает работать так, как будто ничего не изменилось.
Взгляните в последний раз на созданные вручную индексы в разметке и попрощайтесь с ними.
Удаляем индексы. Победа!
Это третий, самый современный способ создания подобных элементов. Он не требует ни вручную созданных «батарей» стилей, ни стилей созданных с помощью препроцессора, ни засорения разметки атрибутами с переменными-индексами.
А ещё этот подход предоставляет невероятную гибкость. Давайте немного поэкспериментируем с ним.
Попробуем добавить элементы в разметку. Ничего не изменилось. Дополнительные лепестки «наложились» на первые лепестки.
Попробуем удалить элементы из разметки. В лоадере появилась «дырка».
Благодаря sibling-функциям мы можем сделать текущее решение ещё более гибким! Например, можно сделать так, чтобы при добавлении лепестков лоадер становился «плотнее», а при удалении — более «разреженным».
Для этого нужно пересмотреть статичные значения шагов в формулах. Например, шаг поворота 30deg был выбран, исходя из фиксированного количества лепестков — 12.
Введём новую переменную --full-angle. В неё запишем полный угол, на который должны поворачиваться лепестки в лоадере. Поскольку у нас полный круг, значение переменной равно 360deg.
Теперь нужно рассчитать шаг поворота, который зависит от количества лепестков. Здесь нам и пригодится вторая функция — sibling-count()!
Делим полный угол на количество лепестков и сохраняем результат в переменной --rotate-step.
Теперь подставим --rotate-step в формулу для расчёта поворота конкретного элемента. Дырка в лоадере исчезла, а лепестки распределились равномерно. Это действительно похоже на магию!
Задержка анимации также должна зависеть от количества лепестков. Проделываем ту же процедуру: выносим в переменную --full-duration полную длительность анимации.
Вычислим шаг задержки анимации и сохраним в переменную --delay-step.
Подставим --delay-step в формулу расчёта задержки для конкретного элемента.
Заменим последнее статичное значение в формуле на переменную --full-duration.
Теперь формула полностью готова, и задержка каждого элемента зависит только от общей длительности анимации и порядкового номера элемента.
Давайте протестируем!
Добавим в разметку четыре лоадера с разным количеством лепестков: 4, 6, 12 и 16.
Всё отображается идеально: лепестки равномерно распределяются по кругу, и время анимации у каждого лоадера совпадает.
Но можно пойти дальше!
Во время рефакторинга мы случайно получили два глобальных параметра компонента — размер полного угла и время анимации. Попробуем изменить их для разных элементов. Пусть первый лоадер будет занимать четверть круга и иметь медленную анимацию.
Второй лоадер ускорим и сделаем шире первого.
Для третьего лоадера зададим отрицательное значение угла — -360deg. Удивительно, ничего не сломалось, и анимация начала идти в противоположном направлении.
Последнему лоадеру зададим очень большой угол и получим эффект «стробоскопа».
Функции sibling-index() и sibling-count() — это просто чудо! Согласны? Осталось дождаться, когда их будут поддерживать все браузеры.