Ключевые кадры: @keyframes и animation
Переходы реагируют на смену состояния, а ключевые кадры позволяют запустить анимацию саму по себе и описать сколько угодно этапов.
@keyframes — это именованный сценарий анимации, набор «снимков» свойств в разные моменты её хода, от 0% до 100%.
transition хорош для «было → стало», но у него ровно две точки и нужен внешний триггер. Когда требуется загрузочный спиннер, пульсирующая точка уведомления или многоэтапное появление — нужны @keyframes и свойство animation. Они работают сами, без :hover и без JavaScript.
Зачем это на практике
Бесконечное вращение, мигание, «дыхание» кнопки, последовательное появление пунктов меню — всё это анимации без явного начального и конечного состояния DOM. Их нельзя выразить через transition, потому что там нет переключения класса в нужный момент. Ключевые кадры решают задачу декларативно.
Объявляем сценарий
Сначала описываем сам сценарий — что и в какой момент времени делает свойство. Проценты — это доля от полной длительности анимации.
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
from и to — синонимы 0% и 100%. Затем привязываем сценарий к элементу:
.toast {
animation-name: fade-in-up;
animation-duration: 0.4s;
animation-timing-function: ease-out;
}
Свойства animation
| Свойство | Что задаёт |
animation-name | имя сценария из @keyframes |
animation-duration | длительность одного прохода |
animation-timing-function | кривая скорости (как у transition) |
animation-delay | задержка перед стартом |
animation-iteration-count | сколько раз повторить; infinite — бесконечно |
animation-direction | направление: normal, reverse, alternate |
animation-fill-mode | какие стили держать до старта и после конца |
animation-play-state | running или paused — пауза анимации |
Многошаговая анимация
Сила @keyframes — в произвольных промежуточных кадрах. Опишем подпрыгивающий мячик: вверх он замедляется, вниз ускоряется, в точке касания «сплющивается».
@keyframes bounce {
0% { transform: translateY(0) scaleY(1); }
40% { transform: translateY(-60px) scaleY(1); }
55% { transform: translateY(-60px) scaleY(1); }
100% { transform: translateY(0) scaleY(0.92); }
}
.ball {
animation: bounce 0.9s ease-in-out infinite alternate;
}
Здесь сразу видно сокращённую запись: animation: name duration timing-function iteration-count direction. Несколько кадров с одинаковым значением (40% и 55%) создают паузу «в верхней точке».
Бесконечный цикл
Классический спиннер — вращение по кругу без конца. Здесь нужны именно infinite и linear: с ease вращение бы дёргалось на стыке циклов.
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid #e5e7eb;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
Достаточно описать только to — начальное состояние браузер возьмёт из текущих стилей элемента (поворот 0deg).
direction и fill-mode на практике
animation-direction: alternate заставляет анимацию проигрываться вперёд, затем назад, и так по кругу — идеально для пульсации, чтобы не было резкого скачка к началу.
animation-fill-mode решает важную проблему: по умолчанию до старта и после конца элемент показан своими обычными стилями, игнорируя кадры. Если анимация с задержкой, элемент успеет «мигнуть» конечным видом перед стартом. Значения:
forwards— после конца оставить стили последнего кадра (частый выбор для появлений).backwards— во время задержки уже показать стили первого кадра.both— и то и другое.
.menu-item {
opacity: 0;
animation: fade-in-up 0.3s ease-out forwards;
}
.menu-item:nth-child(2) { animation-delay: 0.08s; }
.menu-item:nth-child(3) { animation-delay: 0.16s; }
Без forwards пункты после анимации вернулись бы к opacity: 0 и исчезли. Разные задержки дают эффект «лесенки» — пункты появляются один за другим.
play-state и пауза
animation-play-state позволяет приостановить и возобновить анимацию, не сбрасывая её прогресс. Удобно для «бегущей строки», которая замирает под курсором, чтобы текст можно было прочитать:
.marquee {
animation: scroll-left 12s linear infinite;
}
.marquee:hover {
animation-play-state: paused; /* остановить, не теряя позицию */
}
Несколько анимаций на одном элементе
Как и у transition, у animation можно перечислить несколько сценариев через запятую — каждый со своими настройками. Это позволяет разложить сложное движение на независимые слои, например медленное вращение фона плюс быстрая пульсация:
@keyframes pulse { 50% { transform: scale(1.1); } }
.badge {
animation:
spin 6s linear infinite,
pulse 1.2s ease-in-out infinite;
}
Браузер проигрывает их одновременно и независимо. Главное — чтобы они меняли разные свойства или согласованно работали с transform (две анимации, пишущие в transform разными функциями, конфликтуют — выигрывает последняя в списке).
Как это работает под капотом
Браузер раскладывает заданные кадры на временной шкале по процентам. Между соседними кадрами он интерполирует значения той же кривой timing-function, что и в переходах (причём кривую можно задать даже внутри отдельного кадра). В отличие от transition, анимация запускается, как только элемент с правилом animation появляется в DOM или к нему добавляется класс с этим правилом, — внешний «толчок» не нужен. Когда проходов больше одного, в конце каждого цикла значения мгновенно возвращаются к кадру 0% (если direction не alternate), что и вызывает «дёрганье» при неудачной кривой.
Частые ошибки
- Забыли
animation-fill-mode: forwards. Элемент после красивого появления возвращается к исходному виду. - Опечатка в
animation-name. Имя не совпадает с@keyframes— анимации просто нет, без ошибки в консоли. easeна бесконечном вращении. На стыке циклов скорость скачет; дляinfinite-вращений беритеlinear.- Тяжёлые свойства в кадрах. Анимация
top/left/widthв бесконечном цикле грузит процессор; используйтеtransform.
Итоги
@keyframes имя { ... }описывает сценарий,animationпривязывает его к элементу.- Проценты кадров — доли от длительности;
from/toравны0%/100%. iteration-count: infiniteдаёт бесконечный цикл,direction: alternate— движение туда-обратно.fill-mode: forwardsсохраняет финальный кадр после окончания.- Анимация стартует сама, без триггера, и поддерживает любое число промежуточных кадров.