Как сделать анимацию в HTML с помощью CSS

Как сделать анимацию в HTML без лишнего JavaScript? Используйте transition для плавной смены двух состояний, а animation и @keyframes – для самостоятельных сценариев. Вы соберете hover-эффекты, пульсацию, появление при прокрутке и запуск по клику. Код учитывает производительность и настройку prefers-reduced-motion.

Как работает анимация в HTML: transition и animation

CSS-анимация меняет свойства элемента во времени. Выбирайте механизм по тому, кто запускает движение и сколько состояний оно содержит.

Параметрtransitionanimation
ЗапускПосле изменения состояния, например при наведенииПосле назначения свойства или класса
СостоянияИсходное и конечноеДва кадра или последовательность кадров
ПовторыПереход сам не повторяетсяanimation-iteration-count задает повторы
JavaScriptСкрипт меняет класс или свойствоСкрипт запускает и отслеживает сценарий

Берите transition, когда элемент отвечает на действие пользователя. Выбирайте animation, когда эффект стартует сам или проходит несколько фаз.

Синтаксис описан в справочниках MDN по transition и MDN по animation.

Плавные эффекты при наведении на transition

Переход состоит из свойства, длительности, функции времени и задержки. Указывайте конкретные свойства, чтобы изменение другого стиля не запускало лишний эффект.

.card {
  transition-property: transform, box-shadow;
  transition-duration: 300ms;
  transition-timing-function: ease;
  transition-delay: 0ms;
}

.card {
  transition: transform 300ms ease 0ms;
}

Запись 300ms равна 0.3s. Без длительности переход получает значение 0s и остается незаметным.

Квадрат превращается в круг

Контейнер обрезает изображение, а border-radius меняет форму рамки. Ссылка получает такой же эффект при фокусе с клавиатуры.

<a class="shape-demo" href="photo.jpg">
  <img src="photo.jpg" alt="Фотография в квадратной рамке">
</a>
.shape-demo {
  display: block;
  width: 200px;
  height: 200px;
  overflow: hidden;
  border-radius: 16px;
  transition: border-radius 1s ease;
}

.shape-demo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@media (hover: hover) {
  .shape-demo:hover {
    border-radius: 50%;
  }
}

.shape-demo:focus-visible {
  border-radius: 50%;
  outline: 3px solid #2457d6;
  outline-offset: 4px;
}
Изображение с квадратными и круглыми очертаниями

Увеличение изображения через transform

Свойство transform: scale() увеличивает изображение внутри контейнера. Размер контейнера не меняется, поэтому соседние элементы не сдвигаются.

<a class="zoom-demo" href="photo.jpg">
  <img src="photo.jpg" alt="Фотография для увеличения">
</a>
.zoom-demo {
  display: block;
  width: 320px;
  max-width: 100%;
  overflow: hidden;
}

.zoom-demo img {
  display: block;
  width: 100%;
  transition: transform 400ms ease;
}

@media (hover: hover) {
  .zoom-demo:hover img {
    transform: scale(1.08);
  }
}

.zoom-demo:focus-visible img {
  transform: scale(1.08);
}

.zoom-demo:focus-visible {
  outline: 3px solid #2457d6;
  outline-offset: 4px;
}
Изображение с плавным увеличением внутри контейнера

Наклон и черно-белое изображение

rotate() поворачивает элемент на заданный угол. Функция grayscale() убирает цвет изображения.

<a class="tilt-demo" href="photo.jpg">
  <img src="photo.jpg" alt="Цветная фотография">
</a>
.tilt-demo {
  display: inline-block;
  max-width: 100%;
  transition: transform 300ms ease, filter 300ms ease;
}

.tilt-demo img {
  display: block;
  width: 200px;
  max-width: 100%;
}

@media (hover: hover) {
  .tilt-demo:hover {
    transform: rotate(6deg);
    filter: grayscale(100%);
  }
}

.tilt-demo:focus-visible {
  transform: rotate(6deg);
  filter: grayscale(100%);
  outline: 3px solid #2457d6;
  outline-offset: 4px;
}
Изображение с эффектом наклона
Изображение с черно-белым фильтром

Тень, hover для мыши и фокус для клавиатуры

Псевдокласс :hover реагирует на указатель. Сенсорный экран может не поддерживать наведение, поэтому ограничьте правило медиазапросом (hover: hover). Для клавиатуры добавьте :focus-visible.

<button class="button" type="button">Попробовать эффект</button>
.button {
  padding: 12px 20px;
  border: 1px solid #2457d6;
  border-radius: 8px;
  transition: transform 200ms ease, box-shadow 200ms ease;
}

@media (hover: hover) {
  .button:hover {
    transform: translateY(-2px);
    box-shadow: 0 8px 16px rgb(0 0 0 / 20%);
  }
}

.button:focus-visible {
  transform: translateY(-2px);
  box-shadow: 0 8px 16px rgb(0 0 0 / 20%);
  outline: 3px solid #2457d6;
  outline-offset: 3px;
}

Своя анимация на @keyframes

Блок @keyframes описывает кадры, а свойство animation подключает их к элементу. Используйте from и to для двух состояний или проценты для последовательности кадров.

Полный синтаксис смотрите в справочнике MDN по @keyframes.

СвойствоНазначение
animation-nameИмя блока @keyframes
animation-durationДлительность одного цикла
animation-timing-functionТемп движения
animation-delayЗадержка старта
animation-iteration-countЧисло повторов или infinite
animation-directionНаправление проигрывания
animation-fill-modeСостояние до старта и после завершения
animation-play-staterunning или paused

Шортхенд объединяет настройки в одной строке. Первое время задает длительность, второе – задержку.

.message {
  animation: fade-in 600ms ease-out 100ms 1 normal both running;
}

Значение forwards сохраняет завершающий кадр. Значение backwards применяет начальный кадр во время задержки. Значение both объединяет оба поведения.

Плавное появление элемента

<article class="message">
  <h2>Сообщение загружено</h2>
  <p>Карточка появляется со сдвигом.</p>
</article>
@keyframes fade-in {
  from {
    opacity: 0;
    transform: translateY(16px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.message {
  animation: fade-in 600ms ease-out 100ms 1 normal both;
}

Карточка ждет 100 миллисекунд, затем появляется за 600 миллисекунд. Перезагрузка страницы запускает эффект снова.

Бесконечная пульсация кнопки

<button class="pulse-button" type="button">Попробовать пульсацию</button>
@keyframes pulse {
  0%, 100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.05);
  }
}

.pulse-button {
  animation: pulse 1.8s ease-in-out infinite;
}

.pulse-button:focus-visible {
  animation-play-state: paused;
}

Кнопка увеличивается в середине цикла. Фокус клавиатуры приостанавливает декоративное движение.

Спиннер загрузки

<p role="status">
  <span class="spinner" aria-hidden="true"></span>
  Загрузка…
</p>
@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

.spinner {
  display: inline-block;
  width: 32px;
  height: 32px;
  border: 4px solid #d7dbe5;
  border-top-color: #2457d6;
  border-radius: 50%;
  animation: spin 800ms linear infinite;
}

Кольцо делает оборот за 800 миллисекунд. После загрузки уберите статус из интерфейса.

Готовые анимации из Animate.css

Animate.css содержит готовые CSS-классы. Библиотека использует префикс animate__ и базовый класс animate__animated. Исходный код хранится в репозитории Animate.css на GitHub.

Подключение через npm и локальный файл

Если проект использует сборщик, установите пакет через npm. Выполните команду в терминале из папки проекта.

npm install animate.css

Импортируйте библиотеку во входном JavaScript-файле.

import 'animate.css';

Для страницы без сборщика скачайте файл библиотеки с официальной страницы Animate.css и подключите его в head. Скопированный файл можно положить в папку css.

<link rel="stylesheet" href="css/animate.min.css">

После подключения добавьте базовый класс и имя эффекта к элементу.

<h2 class="animate__animated animate__bounce">
  Заголовок подпрыгивает
</h2>

Заголовок проигрывает эффект при загрузке страницы. Удаление классов возвращает обычное состояние.

Задержка, скорость и повторы

Класс animate__delay-2s задает задержку 2 секунды. Классы animate__slow и animate__fast меняют скорость. Класс animate__repeat-2 повторяет эффект 2 раза.

<p class="animate__animated animate__bounce animate__delay-2s animate__slow animate__repeat-2">
  Медленный эффект с задержкой
</p>
<p class="animate__animated animate__bounce animate__fast">
  Быстрый эффект без задержки
</p>

Одному элементу назначайте только один класс скорости. Собственные значения задавайте через CSS-переменную --animate-duration.

Поочередное появление элементов

Разные задержки запускают пункты списка по очереди. Длительность меняется без правки файла библиотеки.

<ul class="features">
  <li class="animate__animated animate__fadeInUp animate__delay-1s">Первый пункт</li>
  <li class="animate__animated animate__fadeInUp animate__delay-2s">Второй пункт</li>
  <li class="animate__animated animate__fadeInUp animate__delay-3s">Третий пункт</li>
</ul>
.features {
  --animate-duration: 700ms;
}

Первый пункт начнет появление через 1 секунду. Второй стартует через 2 секунды, третий – через 3 секунды. Каждый эффект длится 700 миллисекунд.

Запуск анимации по клику и при прокрутке

JavaScript добавляет класс, удаляет его для перезапуска и слушает событие animationend. Один CSS-класс хранит сценарий, а скрипт отвечает за момент запуска.

Добавление класса по клику

<button class="run-button" type="button">Запустить</button>
<div class="box">Готово к запуску</div>
<p class="run-status" role="status"></p>
@keyframes pop {
  0% {
    transform: scale(.8);
    opacity: 0;
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}

.box.is-active {
  animation: pop 400ms ease-out both;
}
const button = document.querySelector('.run-button');
const box = document.querySelector('.box');
const status = document.querySelector('.run-status');

button.addEventListener('click', () => {
  box.classList.remove('is-active');

  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
    status.textContent = 'Готово, движение отключено';
    return;
  }

  void box.offsetWidth;
  box.classList.add('is-active');
  status.textContent = 'Анимация идет';
});

box.addEventListener('animationend', (event) => {
  if (event.animationName === 'pop') {
    status.textContent = 'Анимация завершена';
  }
});

Удаление класса и чтение offsetWidth фиксируют промежуточное состояние. Повторное добавление класса запускает эффект снова.

Чтение offsetWidth может вызвать синхронный пересчет раскладки. Используйте такой перезапуск для отдельных действий, а не для каждого кадра.

Появление при прокрутке

IntersectionObserver сообщает, когда элемент попадает в область просмотра. После первого запуска наблюдатель прекращает работу для этого элемента.

<section class="reveal">
  <h2>Раздел страницы</h2>
  <p>Блок появляется при прокрутке.</p>
</section>
@keyframes reveal-in {
  from {
    opacity: 0;
    transform: translateY(24px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.reveal.is-pending {
  opacity: 0;
  transform: translateY(24px);
}

.reveal.is-visible {
  animation: reveal-in 600ms ease-out both;
}
const sections = document.querySelectorAll('.reveal');

if ('IntersectionObserver' in window) {
  const observer = new IntersectionObserver((entries, currentObserver) => {
    entries.forEach((entry) => {
      if (!entry.isIntersecting) return;
      entry.target.classList.remove('is-pending');
      entry.target.classList.add('is-visible');
      currentObserver.unobserve(entry.target);
    });
  });

  sections.forEach((element) => {
    element.classList.add('is-pending');
    observer.observe(element);
  });
}

Скрывающий класс добавляет скрипт только после проверки API. Поэтому без JavaScript содержимое остается видимым.

Web Animations API дает альтернативный способ: вызов element.animate() запускает кадры из JavaScript.

Производительность плавной CSS-анимации

Изменение размеров заставляет браузер пересчитать геометрию страницы. Такой пересчет называют reflow, а повторную отрисовку областей – repaint.

Для движения выбирайте transform и opacity. Они не меняют раскладку соседних элементов и часто обрабатываются на этапе композиции.

<button class="smooth-card" type="button">Плавное движение</button>
.smooth-card {
  transition: transform 250ms ease, opacity 250ms ease;
}

@media (hover: hover) {
  .smooth-card:hover {
    transform: translateY(-6px);
    opacity: .9;
  }
}

.smooth-card:focus-visible {
  transform: translateY(-6px);
  opacity: .9;
}

Анимация width, height, top и left может запускать перерасчет раскладки. Тени, фильтры и скругления иногда требуют дополнительной отрисовки.

Не назначайте will-change сотням элементов. Такое правило расходует память и иногда ухудшает производительность.

  1. Откройте Chrome DevTools на странице с эффектом.
  2. Перейдите на вкладку Performance и начните запись.
  3. Запустите анимацию и остановите запись.
  4. Выберите участок движения и найдите события Layout и Paint.
  5. Замените изменение размеров на transform, повторите запись и сравните результат.

Длинные события Layout или Paint занимают время кадра и могут объяснить рывки. Панель Animations помогает увидеть кадры и задержки, а Performance показывает нагрузку на раскладку.

Сведения о поддержке браузерами собраны в базе Can I Use. В примерах используются стандартные transition, animation, transform и filter без вендорных префиксов.

Анимация и доступность: prefers-reduced-motion

Некоторые пользователи уменьшают движение в системных настройках. Медиазапрос prefers-reduced-motion позволяет остановить декоративные эффекты и сохранить смысловую обратную связь.

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }

  .pulse-button,
  .spinner,
  .box.is-active,
  .reveal.is-visible {
    animation: none !important;
  }

  .reveal.is-pending,
  .reveal.is-visible {
    opacity: 1;
    transform: none;
  }
}

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

Текстовый статус «Загрузка…» сохраняет смысл спиннера без движения. Подробности о медиазапросе описаны в документации MDN по prefers-reduced-motion.

Почему CSS-анимация не срабатывает

Проблему обычно вызывают имя, время или состояние элемента. Проверьте эти причины по порядку.

  • Не задана длительность. Добавьте animation-duration или время в шортхенд. Значение по умолчанию равно 0s.
  • Опечатка в имени. Значение animation-name должно совпадать с именем блока @keyframes.
  • Анимируется display. Переключение происходит дискретно. Для плавного исчезновения используйте opacity.
  • Высота равна auto. Для простого перехода задайте числовые значения начальной и конечной высоты.
  • Класс уже добавлен. Повторное присваивание того же класса не перезапускает эффект. Сначала удалите класс, затем добавьте его снова.
  • Несколько анимаций меняют transform. Объедините преобразования в одних кадрах или распределите их между элементом и оберткой.
  • Эффект работает только на hover. Добавьте :focus-visible и обработчик клика для действия на сенсорном экране.
  • JavaScript не нашел элемент. Проверьте селектор и подключите скрипт после разметки или с атрибутом defer.
  • Движение отключено настройками. Убедитесь, что правило prefers-reduced-motion не остановило эффект намеренно.

Совет от нашей редакции: сначала соберите короткий эффект на transform и opacity, затем добавляйте задержки и декоративные кадры. Такой порядок упрощает поиск ошибок и сохраняет отзывчивость интерфейса.

Комментарии

Оставьте свой комментарий
Пока никто не оставил комментариев