Анимация CSS: transition и @keyframes с примерами

опубликовано
обновлено
читать
≈ 5 мин
автор
среда и версия
CSS; пример, две правки и режим уменьшенного движения проверены в HTML/CSS-песочнице Koddo (Chromium) 20 октября 2026.

Кнопка меняет цвет при наведении мгновенно, рывком. Добавляешь одну строку transition — и цвет перетекает за долю секунды. Для движения посложнее, вроде пульса или вращения, есть @keyframes.

В CSS два способа анимировать: transition плавно проводит свойство от старого значения к новому, когда оно меняется, например при наведении. @keyframes вместе с animation описывает движение по кадрам и запускает его само, хоть бесконечно. JavaScript не нужен ни там, ни там.

Пример: кнопка, значок и спиннер

Открой пример в песочнице HTML/CSS ссылкой под вторым блоком: разметка попадёт во вкладку index.html, стили — в styles.css. Анимация пойдёт в результате сразу. Наведи курсор на кнопку: она плавно потемнеет и чуть приподнимется, а значок «новое» и спиннер двигаются сами.

HTML:

<main class="demo">
  <button class="button" type="button">наведи на меня</button>
  <p class="badge">новое</p>
  <div class="spinner" role="status" aria-label="Загрузка"></div>
</main>

CSS:

body {
  margin: 0;
  font-family: system-ui, sans-serif;
}

.demo {
  display: grid;
  justify-items: center;
  gap: 1.5rem;
  padding: 2rem;
}

.button {
  padding: 0.75rem 1.5rem;
  border: 0;
  border-radius: 999px;
  background: #3a86ff;
  color: #ffffff;
  font-size: 1rem;
  cursor: pointer;
  transition: background-color 0.4s ease, transform 0.4s ease;
}

.button:hover {
  background: #1d4ed8;
  transform: translateY(-4px);
}

.badge {
  margin: 0;
  padding: 0.25rem 0.75rem;
  border-radius: 999px;
  background: #ff006e;
  color: #ffffff;
  animation: pulse 1.5s ease-in-out infinite;
}

@keyframes pulse {
  0%,
  100% {
    transform: scale(1);
  }

  50% {
    transform: scale(1.15);
  }
}

.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #e5e7eb;
  border-top-color: #3a86ff;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

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

@media (prefers-reduced-motion: reduce) {
  .badge,
  .spinner {
    animation: none;
  }
}
Анимация / 01

Переход ждёт изменения, анимация идёт сама

Чем transition отличается от @keyframesШаг 1: transition у кнопки проводит фон от #3a86ff к #1d4ed8 за 0,4 секунды, когда курсор наводят на кнопку, и обратно, когда его уводят. Шаг 2: @keyframes pulse задаёт три кадра на шкале 1,5 секунды — масштаб 1 в начале, 1,15 в середине и снова 1 в конце; animation с infinite повторяет их по кругу без всякого события.01 / transition: от одного значения к другому#3a86ff#1d4ed8:hover · 0,4 собычная кнопкакурсор на кнопкесрабатывает, когда значение меняется, — и так же плавно обратно02 / @keyframes: кадры на шкале времени11.151scale в каждом кадре0% · 0 с50% · 0,75 с100% · 1,5 сinfinite — после 100% снова с 0%, без всякого события
transition ждёт, когда значение изменится, и проводит его от старого к новому. @keyframes задаёт кадры на шкале времени, а animation запускает их сам и с infinite повторяет по кругу.

transition: плавный переход между состояниями

transition говорит браузеру: когда это свойство изменится, не прыгай к новому значению, а дойди до него за указанное время.

.button {
  transition: background-color 0.4s ease, transform 0.4s ease;
}

В записи по порядку: какое свойство, сколько длится переход, как меняется скорость и, если нужно, задержка перед стартом. Несколько свойств перечисляют через запятую. Запись transition: all 0.4s тоже работает, но анимирует вообще всё, что изменится, — и то, чего ты не ждал.

Функция скорости. ease — плавный разгон и торможение, linear — ровно, ease-in — медленный старт, ease-out — медленный финиш. Для своей кривой есть cubic-bezier(…).

Где писать. transition ставят обычному состоянию, .button, а не .button:hover. Тогда кнопка плавно темнеет при наведении и так же плавно светлеет, когда курсор уходит.

@keyframes и animation: движение по кадрам

Анимация из кадров собирается в два шага. Сначала @keyframes с именем описывает кадры: в процентах от длительности или словами from и to, то есть 0% и 100%. Потом свойство animation у элемента включает эти кадры:

@keyframes pulse {
  0%,
  100% {
    transform: scale(1);
  }

  50% {
    transform: scale(1.15);
  }
}

.badge {
  animation: pulse 1.5s ease-in-out infinite;
}

Значок увеличивается к середине и возвращается к исходному размеру за полторы секунды, а infinite повторяет это бесконечно. У спиннера кадр один, to: браузер сам возьмёт начальный поворот 0 и дойдёт до 360 градусов. С linear вращение идёт без рывков на стыке оборотов.

У animation много частей, в короткой записи их пишут через пробел:

СвойствоЧто задаётПример
animation-nameимя из @keyframespulse
animation-durationдлительность одного повтора1.5s
animation-timing-functionкак меняется скоростьease-in-out, linear
animation-delayзадержка перед стартом0.5s
animation-iteration-countсколько раз повторить3, infinite
animation-directionнаправлениеalternate — туда и обратно
animation-fill-modeчто показывать до и послеforwards — остаться в последнем кадре
animation-play-stateпаузаpaused, running

Какие свойства анимировать, чтобы не тормозило

Дешевле всего браузеру даются transform и opacity: элемент не надо заново раскладывать и перерисовывать, браузер двигает уже готовую картинку его слоя. Поэтому двигай через transform: translate(…), увеличивай через scale(…), а показывай и прячь через opacity.

Анимация width, height, top, left или margin заставляет браузер на каждом кадре пересчитывать раскладку страницы. На слабом телефоне это заметно по рывкам. Если блок должен «выехать», лучше сдвинуть его через transform, а не менять left.

Уважай prefers-reduced-motion

Некоторым людям от движения на экране становится плохо, и они включают в системе «уменьшение движения». CSS это видит:

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

В примере при такой настройке значок и спиннер замирают, а переход у кнопки остаётся. Если и он лишний, добавь в этот же блок .button { transition: none; }.

Почему анимация не работает: частые ошибки

transition только в :hover. Наведение будет плавным, а уход курсора — рывком: у обычного состояния перехода нет. Пиши transition в основном правиле.

Забыл infinite. По умолчанию анимация проигрывается один раз и останавливается в исходном виде. Нужен повтор — добавь infinite или число повторов.

Переход к height: auto. Браузеры по умолчанию не анимируют переход к auto, блок раскроется рывком. Анимируй max-height с запасом или transform: scale(…).

display не анимируется. Элемент с display: none появится сразу. Для плавного появления меняй opacity, а скрывай после окончания анимации.

Анимация затирает transform. Если у элемента уже есть transform: translateX(…), а кадры задают transform: scale(…), на время анимации сдвиг пропадёт: кадры заменяют весь transform. Добавь сдвиг в каждый кадр или анимируй отдельное свойство scale.

Три правки, чтобы закрепить

Открой пример в песочнице и перед каждой правкой предскажи результат:

  1. Перенеси строку transition из .button в .button:hover. Кнопка по-прежнему плавно потемнеет, а когда курсор уйдёт, станет светлой мгновенно.
  2. Замени у значка infinite на 3. Значок качнётся три раза и замрёт, а спиннер продолжит крутиться.
  3. Поменяй у спиннера linear на ease-in-out. Каждый оборот начнётся медленно, разгонится и затормозит к концу — заметно, где кончается один оборот и начинается другой.

А transform без всякой анимации пригодится в игре «Повтори макет»: уровень «Ромб» решается одной строкой transform: rotate(45deg). Оформление под анимацию удобно собирать по статьям про селекторы и выравнивание по центру.

Источники