CSS-анимации: от перехода до скролл-эффектов

Анимация превращает статичную страницу в живой интерфейс: кнопки отзываются на действия, карточки плавно появляются, а контент не «прыгает» между состояниями. Начнём с переходов и кривых скорости, разберём @keyframes, научимся анимировать по скроллу и поговорим о производительности и доступности.

Уровень: средний 13 разделов 2–3 часа

1. Зачем анимировать

Анимация — это не украшение, а инструмент коммуникации. Она подсказывает пользователю, что элемент отреагировал на действие, откуда пришло новое содержимое и что происходит в системе прямо сейчас.

Главный принцип: анимация служит цели, а не украшает всё подряд. Если движение не помогает понять интерфейс — оно лишнее и только раздражает. Удалите его, и страница станет только лучше.

2. transition: базовые переходы

Свойство transition плавно переводит элемент из одного состояния в другое при изменении значения: по наведению, фокусу или смене класса. Переход применяется к разнице между двумя наборами CSS-свойств.

.button {
  background: #4f7cff;
  transition-property: background, transform;
  transition-duration: 0.2s;
  transition-timing-function: ease-out;
  transition-delay: 0s;
}

.button:hover {
  background: #2f5ce0;
  transform: translateY(-2px);
}

Короткая запись объединяет все четыре части:

transition: background 0.2s ease-out, transform 0.15s ease-out;
СвойствоЧто задаёт
transition-propertyкакие свойства анимируются
transition-durationдлительность перехода
transition-timing-functionкривая скорости внутри интервала
transition-delayпауза перед началом
Не пишите transition: all .3s: анимироваться будет всё подряд, замедляя страницу и отключая осознанные решения. Перечисляйте свойства явно.
Правило из раздела 4: переходите ограниченный набор свойств — transform, opacity и цветовые значения. Всё остальное делает интерфейс тяжёлым.

3. Кривые скорости

transition-timing-function (и его аналог animation-timing-function) задаёт, как скорость меняется в течение анимации. Одни кривые разгоняются, другие тормозят в конце, третьи — «пружинят», возвращаясь чуть дальше цели.

Ключевое словоПоведение
linearпостоянная скорость, без разгона
easeзначение по умолчанию: плавный разгон и торможение
ease-inмедленный старт, ускорение к концу
ease-outбыстрый старт, плавное торможение — любимец интерфейсов
ease-in-outсимметричный разгон и торможение
cubic-bezier()произвольная кривая через четыре точки

Функция cubic-bezier(x1, y1, x2, y2) описывает кривую Безье двумя контрольными точками. Первые две координаты ограничены отрезком от 0 до 1, вторые могут выходить за него — так получается «отскок назад».

/* мягкий выход — универсальный ease-out для интерфейсов */
transition: transform 0.3s cubic-bezier(0.2, 0, 0, 1);

/* «пружина»: элемент перелетает цель и возвращается */
transition: transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
Готовые ease-outs вроде cubic-bezier(0.2, 0, 0, 1) и «магические» кривые из easing-сборников сэкономят часы подбора. Запомните пару любимых и используйте их везде.

Сравните кривые на живом примере: наведите курсор на ряд, и квадраты пролетят до конца с разными скоростями.

4. transform и opacity: золотая пара

transform и opacity — единственные свойства, которые браузер может анимировать без пересчёта вёрстки. Элемент выносится на отдельный слой, и движение выполняет графический композитор на GPU: никаких пошаговых пересчётов layout и paint.

.card {
  transform-origin: center;
  transition: transform 0.25s ease-out;
}

.card:hover {
  transform: translateY(-6px) scale(1.04);
}

.icon {
  transition: transform 0.2s ease-out;
}

.icon:hover {
  transform: rotate(90deg);
}

transform покрывает движение, поворот, масштаб и наклон: translate(), rotate(), scale(), skew(). transform-origin задаёт точку, вокруг которой происходят повороты и масштабирование — по умолчанию это центр.

Анимация margin, width, height, top, left пересчитывает layout на каждом кадре — это дорого и заметно «рвётся» на слабых устройствах. Перемещайте элементы только через transform: translate().
Opacity тоже экономичен: он просто меняет прозрачность слоя, не заставляя браузер перерисовывать геометрию. Поэтому «золотая пара» transform + opacity закрывает 90% интерфейсных анимаций.

5. @keyframes: ключевые кадры

Если переход плавно меняет одно состояние на другое, то @keyframes описывает целый сценарий из нескольких кадров — от появления до «парения» и обратно. Кадры задаются ключевыми словами from/to или процентами.

@keyframes fade-up {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}

.hero {
  animation: fade-up 0.6s ease-out both;
  animation-delay: 0.15s;
}

Короткая запись: animation принимает имя, длительность, функцию скорости, количество повторений и задержку одним набором значений. Гибкие кадры через проценты:

@keyframes float {
  0%   { transform: translateY(0); }
  50%  { transform: translateY(-12px); }
  100% { transform: translateY(0); }
}

.badge {
  animation: float 3s ease-in-out infinite;
}
fill-modeПоведение до и после анимации
noneэлемент в естественном состоянии (по умолчанию)
forwardsостаётся в состоянии последнего кадра после конца
backwardsприменяет первый кадр ещё до старта (со задержкой)
bothкомбинация backwards и forwards
Почти всегда для появления используйте both: без него элемент может пару кадров мелькнуть в конечном состоянии до запуска анимации, особенно с animation-delay.

6. Повторение и итерации

По умолчанию анимация проигрывается один раз. animation-iteration-count задаёт число повторов, а infinite — бесконечный цикл. Двусторонний цикл задаётся направлением alternate: элемент движется вперёд и назад, а не прыгает в начальную точку.

.icon-spinner {
  animation: spin 0.8s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

.pulse {
  animation: pulse 1.6s ease-in-out infinite alternate;
}

@keyframes pulse {
  from { opacity: 0.35; }
  to   { opacity: 1; }
}

Интерактивная пауза по наведению делается через animation-play-state:

.ticker:hover {
  animation-play-state: paused;
}
Для пульсирующих индикаторов выбирайте opacity вместо transform: анимация прозрачности мягче и дешевле, а в сочетании с alternate даёт ровное «дыхание».

7. Микроанимации интерфейса

Микроанимации — маленькие движения, которые делают интерфейс отзывчивым: подъём карточки по наведению, пульсирующая точка уведомления, спиннер, поворачивающаяся иконка. Общий рецепт: 150–300 миллисекунд, transform и opacity, тихая кривая ease-out.

.card {
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
  transition: transform 0.25s ease-out, box-shadow 0.25s ease-out;
}

.card:hover {
  transform: translateY(-6px);
  box-shadow: 0 10px 22px rgba(0, 0, 0, 0.15);
}

@keyframes pulse {
  0%   { box-shadow: 0 0 0 0 rgba(35, 197, 106, 0.5); }
  100% { box-shadow: 0 0 0 14px rgba(35, 197, 106, 0); }
}

.dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #23c56a;
  animation: pulse 1.4s ease-out infinite;
}

Посмотрите три готовых микроанимации вживую: подъём карточки, pulse-индикатор и спиннер.

8. Появление контента

Появление — самый частый сценарий новых разделов: fade-in со скольжением вверх читается мягче, чем резкое появление. Каскад с задержками (stagger) делает списки «ожившими»: каждый следующий элемент появляется с небольшой паузой после предыдущего.

@keyframes fade-up {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}

.list-item {
  opacity: 0;
  animation: fade-up 0.4s ease-out forwards;
}

.list-item:nth-child(2) { animation-delay: 0.12s; }
.list-item:nth-child(3) { animation-delay: 0.24s; }
.list-item:nth-child(4) { animation-delay: 0.36s; }
Не исчезайте весь блок в opacity: 0 до запуска анимации: если CSS вдруг не загрузится или пользователь отключит анимации (раздел 9), контент останется невидимым. Держите видимое состояние в зоне, отвечающей за «после анимации» через forwards.

9. Уважаем пользователя: prefers-reduced-motion

Часть пользователей страдает от движения — укачивание, вестибулярные расстройства, мигрени. Настройка ОС «Уменьшить движение» отправляет медиазапрос prefers-reduced-motion, и сайт обязан на него откликнуться.

.hero { animation: fade-up 0.6s ease-out; }

@media (prefers-reduced-motion: reduce) {
  .hero,
  .list-item {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

Плюс общее правило безопасности для всей страницы: отключить декоративные анимации одним блоком — но оставить обязательные индикаторы, которые несут информацию.

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
Правило «не отключать обязательные анимации»: спиннер загрузки, мигание предупреждения, индикатор прогресса несут смысл. Укоротите их до минимума или замените текстом, но не прячьте совсем.

Проверьте, как блок ведёт себя с включённым «уменьшением движения» в вашей системе.

10. Анимации по скроллу

Новейшая возможность CSS: animation-timeline: view() и scroll() привязывают ход анимации к прокрутке. Элемент «появляется» по мере въезда во вьюпорт, а прогресс-бар растёт вместе с прокруткой страницы — без единой строчки JavaScript.

@supports (animation-timeline: view()) {
  .reveal {
    animation: fade-up both;
    animation-timeline: view();
    animation-range: entry 10% cover 35%;
  }

  .progress {
    transform-origin: left;
    animation: grow linear both;
    animation-timeline: scroll(root);
  }
}

@keyframes grow {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

view() связывает кадры с пересечением элемента и вьюпорта, scroll(root) — с прокруткой всей страницы. animation-range уточняет, в какой момент пути анимация стартует и завершается. Для параллакс-фрагментов сдвигайте фон через transform: translateY() внутри той же timeline.

Поддержка scroll-анимаций появилась недавно, поэтому всегда используйте @supports. В старых браузерах элемент просто останется в конечном состоянии — это надёжный fallback. Обходной путь на JavaScript — библиотеки, превращающие появление по скроллу.

11. Производительность

Рендер каждого кадра проходит три стадии: layout (пересчёт геометрии), paint (заливка пикселей) и composite (сборка слоёв). Анимация transform/opacity минует первые две и работает только на композиторе — это главный секрет плавных 60 кадров в секунду.

.element {
  /* предупреждаем браузер: свойство будет меняться */
  will-change: transform;
}

.hidden-section {
  /* вне экрана не тратим время на отрисовку */
  content-visibility: auto;
  contain-intrinsic-size: 0 400px;
}
СтадияЧто происходитЧто вызывает
Layoutпересчёт размеров и позицийmargin, width, height, top, left
Paintзаливка пикселейbox-shadow, background, color
Compositeсборка готовых слоёвtransform, opacity
Избегайте анимации высоты, ширины и координат top/left: каждый кадр пересчитывает layout и может обрушить производительность до нескольких кадров в секунду на слабых устройствах.

12. Готовые приёмы и библиотеки

Для частых сценариев не нужно изобретать велосипед: почти всё закрывают готовые подходы и коллекции. Но библиотека — это вес, JavaScript и зависимость от чужих обновлений, поэтому прикидывайте, что дешевле.

Правило «10 строк»: если сценарий покрывается несколькими правилами transform и opacity — пишите сами, не тащите библиотеку. Вес библиотеки, JS-футер и время загрузки почти всегда дороже.

13. Чек-лист перед сдачей

Перед релизом прогоните анимации по контрольному списку. Он закрывает и доступность, и производительность, и аккуратность восприятия.

Хорошая анимация незаметна: пользователь не видит «красивый код», он видит понятный и отзывчивый интерфейс. Если движение ни о чём не сообщает — его не должно быть.

Что дальше

Ты научился оживлять интерфейс: переходы, keyframes, scroll-анимации, доступность и производительность. Закрепи знания по вёрстке на курсе CSS, прокачай раскладку в Flexbox и Grid и собери всё вместе в практике: лендинг.

← Вернуться к курсам