Знакомство 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()
— это просто чудо! Согласны? Осталось дождаться, когда их будут поддерживать все браузеры.