12k
All articles

Каскадные анимации без вычислений в nth-child

Замените цепочки nth-child и циклы JavaScript на sibling-index() для каскадных CSS-анимаций. Разберите варианты, применение в вёрстке, поддержку и запасной вариант.

OpenReplay Team
OpenReplay Team
Каскадные анимации без вычислений в nth-child

Одно правило, animation-delay: calc((sibling-index() - 1) * 80ms), создаёт каскадную анимацию (stagger) для списка любой длины. Вам больше не нужны отдельное правило :nth-child() для каждого элемента или цикл на JavaScript, задающий --i.

Большая часть кода для каскадных анимаций пишется под пять элементов. Затем кто-то добавляет шестую карточку, и она появляется одновременно с первой.

В этой статье мы избавимся от такого кода. На протяжении всей статьи используется один пример: список карточек, каждая из которых появляется чуть позже предыдущей. Мы рассмотрим старый подход и его замену, обратный вариант и вариант «от центра», применение sibling-count() в вёрстке, а также защитное условие, с которым этот код можно выпускать в продакшен. Речь пойдёт только о CSS. Подход для React описан в статье staggered text animations with Framer Motion.

Ключевые выводы

  • sibling-index() возвращает позицию элемента среди соседних элементов, начиная с 1. sibling-count() возвращает количество дочерних элементов родителя, включая сам элемент.
  • Обе функции возвращают обычные числа. Временем или длиной они становятся, только если умножить их на единицу измерения внутри calc().
  • animation-delay: calc((sibling-index() - 1) * 80ms) заменяет отдельное правило :nth-child() для каждой карточки, какой бы длины ни был список.
  • Обе функции появились в Chrome и Edge 138 в июне 2025 года, в Safari 26.2 в декабре 2025 года и в Firefox 154 в августе 2026 года. Это означает статус Baseline newly available.
  • Поместите задержку внутрь @supports (top: calc(1px * sibling-index())). Тогда браузеры без поддержки покажут все карточки одновременно, и вёрстка не сломается.

Старый подход: лесенки nth-child и встроенные пользовательские свойства

До появления sibling-index() каскадная анимация на CSS означала, что задержку для каждой позиции приходилось прописывать вручную. Для этого было два способа: лесенка из :nth-child() или пользовательское свойство с индексом, записанное прямо в разметку. Лесенка использует :nth-child(), который, как и знакомые большинству разработчиков селекторы соседних элементов CSS, выбирает элементы по их положению среди соседей:

.card { animation: fade-in 400ms ease both; }
.card:nth-child(2) { animation-delay: 80ms; }
.card:nth-child(3) { animation-delay: 160ms; }
.card:nth-child(4) { animation-delay: 240ms; }
.card:nth-child(5) { animation-delay: 320ms; }

Лесенка заканчивается на той длине, под которую её написали. Вариант с пользовательским свойством переносит подсчёт в HTML:

<li class="card" style="--i: 0">…</li>
<li class="card" style="--i: 1">…</li>
<li class="card" style="--i: 2">…</li>
<li class="card" style="--i: 3">…</li>
.card { animation-delay: calc(var(--i) * 80ms); }

В случае динамических списков индекс обычно приходится задавать скриптом:

document.querySelectorAll('.card').forEach((el, i) => el.style.setProperty('--i', i));

Как sibling-index() заменяет лесенку nth-child?

sibling-index() заменяет всю лесенку :nth-child() одним объявлением: animation-delay: calc((sibling-index() - 1) * 80ms). Функция определена в разделе tree-counting functions спецификации CSS Values and Units Level 5. sibling-index() присваивает первому соседнему элементу номер 1, как и :nth-child(), и при подсчёте пропускает текстовые узлы и комментарии.

@keyframes fade-in {
  from { opacity: 0; translate: 0 8px; }
}

.card {
  animation: fade-in 400ms ease both;
  animation-delay: calc((sibling-index() - 1) * 80ms);
}

Функция возвращает целое число без единиц измерения, поэтому именно умножение внутри calc() превращает его во время.

Единица вычитается, чтобы у первой карточки не было задержки. При пяти карточках индексы от 1 до 5 превращаются в 0, 80, 160, 240 и 320ms. Без - 1 каждая карточка ждёт на один шаг дольше, и список 80ms остаётся неподвижным, прежде чем что-либо произойдёт.

Ключевое слово both в сокращённой записи тоже важно. Когда animation-fill-mode имеет значение both или backwards, первый ключевой кадр применяется на время задержки, поэтому ожидающие карточки остаются невидимыми. Без него последующие карточки отображаются с полной непрозрачностью, а затем резко исчезают, когда начинается их анимация.

Для длинных списков задержку можно ограничить, чтобы 40-я карточка не ждала больше трёх секунд:

.card { animation-delay: calc(min(sibling-index() - 1, 10) * 80ms); }

Как запустить каскад в обратном порядке или от центра?

Чтобы обратить каскад на основе sibling-index(), вычтите индекс из sibling-count(). Выражение calc((sibling-count() - sibling-index()) * 80ms) даёт последней карточке задержку 0ms, поэтому последняя карточка анимируется первой, а первая — последней. Для каскада от центра вычислите расстояние каждой карточки до середины:

.card { --centre: calc((sibling-count() + 1) / 2); }

.list--reverse .card {
  animation-delay: calc((sibling-count() - sibling-index()) * 80ms);
}

.list--centre .card {
  animation-delay: calc(
    max(sibling-index() - var(--centre), var(--centre) - sibling-index()) * 80ms
  );
}

При пяти карточках центр равен 3, поэтому задержки составляют 160, 80, 0, 80 и 160ms. При чётном количестве центр оказывается между двумя карточками. Четыре карточки дают 120, 40, 40 и 120ms, и две средние карточки стартуют одновременно.

Использование sibling-count() в вёрстке

sibling-count() также можно использовать как значение для вёрстки. Функция считает все дочерние элементы родителя, включая сам элемент, как описано в справочнике MDN. Деление на это значение даёт равные доли: width: calc(100% / sibling-count()). Вот как это выглядит на том же списке карточек с учётом отступа между ними:

.list { display: flex; --gap: 1rem; gap: var(--gap); }

.card {
  width: calc((100% - (sibling-count() - 1) * var(--gap)) / sibling-count());
  --progress: calc(sibling-index() / sibling-count() * 100%);
}

.card::after { content: ""; display: block; height: 3px; width: var(--progress); }

В обычной flex-строке равную ширину и так даёт flex: 1. Формула полезна там, где flexbox не справляется: для абсолютно позиционированных или перекрывающихся карточек, а также когда ширина используется в других вычислениях. --progress задаёт каждой карточке её долю от общего количества, поэтому карточка 3 из 5 рисует полосу шириной 60%.

sibling-count() и sibling-index() считают соседей самого элемента, а не его дочерние элементы. Если применить sibling-count() к <ul>, вы получите количество дочерних элементов родителя списка (включая сам список), а не количество элементов <li> в нём. Вычисления, зависящие от числа дочерних элементов, размещайте в правилах для самих дочерних элементов.

Выпуск в продакшен: поддержка браузерами и защита через @supports

sibling-index() и sibling-count() поддерживаются всеми тремя основными движками, поэтому имеют статус Baseline newly available.

ДвижокВерсияДата выхода
Chrome / Edge138Июнь 2025
Safari (macOS, iOS)26.2Декабрь 2025
Firefox154Август 2026

Пользователи старых версий всё ещё существуют, поэтому оставьте саму анимацию появления вне защитного блока, а внутрь поместите только каскадную задержку:

.card { animation: fade-in 400ms ease both; }

@supports (top: calc(1px * sibling-index())) {
  .card { animation-delay: calc((sibling-index() - 1) * 80ms); }
}

Проверка оборачивает функцию в calc(), результатом которого является длина, и применяет его к свойству, принимающему длины. Браузер, который не может разобрать такое объявление, считает условие ложным и пропускает блок. Все карточки по-прежнему появляются, просто одновременно.

Учёт prefers-reduced-motion

При prefers-reduced-motion: reduce установка animation: none для карточек сразу отображает их все в конечном состоянии:

@media (prefers-reduced-motion: reduce) {
  .card { animation: none; }
}

Заключение

Теперь каскадная анимация умещается в одно защищённое объявление, которое работает с любым количеством карточек. Удалите лесенку :nth-child() и скрипт, записывающий --i, добавьте блок @supports и правило для reduced motion — и в разметке больше не понадобятся атрибуты с индексами. Если значительная часть ваших анимаций всё ещё зависит от JavaScript, прочитайте статью replacing animation libraries with native web APIs.

Часто задаваемые вопросы

Чем sibling-index() отличается от CSS-функции counter()?

counter() возвращает текст, поэтому пригодна только внутри свойства content. sibling-index() возвращает число, с которым можно выполнять вычисления, например calc((sibling-index() - 1) * 80ms). Значение счётчика нельзя использовать для animation-delay, ширины или угла. sibling-index() можно, и для неё не нужны правила counter-reset или counter-increment.

Может ли sibling-index() пропускать определённые элементы, как это делает :nth-child(An+B of S)?

Нет. sibling-index() и sibling-count() не принимают аргументов, поэтому отфильтровать их по селектору нельзя. Они считают все соседние элементы у одного родителя независимо от класса. Заголовок, разделитель или элемент-обёртка внутри списка сдвигает индекс всех последующих карточек и увеличивает общее количество. Анимируемые элементы должны быть единственными дочерними элементами своего контейнера.

Можно ли с помощью sibling-index() управлять цветом и поворотом, а не только задержками?

Да. Это целое число работает в любом выражении calc(), поэтому умножение на единицу измерения даёт угол или длину, например rotate: calc(sibling-index() * 15deg). Деление на sibling-count() равномерно распределяет значение по списку. Например, calc(sibling-index() / sibling-count() * 360deg) задаёт каждому элементу собственный оттенок или собственную позицию на окружности.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.