CSS animation позволяет создавать анимации без JavaScript. Принцип: описываешь ключевые кадры через @keyframes, потом применяешь анимацию к элементу. Загрузчики, плавное появление, пульсация, встряска — всё это чистый CSS.

- @keyframes — описание анимации
- animation — свойства применения
- animation-iteration-count — повторения
- animation-direction — направление
- animation-fill-mode — состояние до и после
- Готовые анимации для копирования
- Несколько анимаций одновременно
- Управление анимацией через JavaScript
- prefers-reduced-motion — уважай пользователей
- Часто задаваемые вопросы о CSS animation
- Как запустить CSS animation по клику?
- animation-fill-mode: forwards — что значит?
- CSS animation vs JavaScript animation — что быстрее?
@keyframes — описание анимации
@keyframes задаёт ключевые кадры анимации — состояния элемента в определённые моменты времени. from/to — начало и конец. Или проценты для точного контроля.
/* from / to — простейший случай */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Проценты — детальный контроль */
@keyframes slideIn {
0% { transform: translateX(-100%); opacity: 0; }
80% { transform: translateX(10px); opacity: 1; }
100% { transform: translateX(0); opacity: 1; }
}
/* Одинаковые значения в нескольких точках */
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-30px); }
}
/* Несколько свойств */
@keyframes highlight {
0% { background-color: transparent; transform: scale(1); }
50% { background-color: #fef08a; transform: scale(1.02); }
100% { background-color: transparent; transform: scale(1); }
}

animation — свойства применения
/* Развёрнутая запись */
.element {
animation-name: fadeIn; /* имя @keyframes */
animation-duration: 0.5s; /* длительность одного цикла */
animation-timing-function: ease; /* кривая скорости */
animation-delay: 0.2s; /* задержка перед стартом */
animation-iteration-count: 1; /* количество повторений */
animation-direction: normal; /* направление */
animation-fill-mode: forwards; /* состояние до/после */
animation-play-state: running; /* running или paused */
}
/* Shorthand: name duration timing delay count direction fill */
.element {
animation: fadeIn 0.5s ease 0.2s 1 normal forwards;
}
/* Минимум: name + duration */
.spinner {
animation: spin 1s linear infinite;
}

animation-iteration-count — повторения
.element { animation-iteration-count: 1; } /* один раз (по умолчанию) */
.element { animation-iteration-count: 3; } /* три раза */
.element { animation-iteration-count: infinite; } /* бесконечно */
.element { animation-iteration-count: 1.5; } /* 1 и ещё половина цикла */
/* Практические примеры */
.spinner { animation: spin 0.8s linear infinite; } /* крутится вечно */
.flash { animation: flash 0.3s ease 3; } /* мигает 3 раза */
.fade-in { animation: fadeIn 0.4s ease 1 forwards; }/* один раз, остаётся */

animation-direction — направление
.a { animation-direction: normal; } /* → → → каждый цикл с начала */
.b { animation-direction: reverse; } /* ← ← ← каждый цикл с конца */
.c { animation-direction: alternate; } /* → ← → ← туда-обратно */
.d { animation-direction: alternate-reverse;}/* ← → ← → обратно-туда */
/* alternate — для пульсации и дыхания */
@keyframes breathe {
from { transform: scale(1); }
to { transform: scale(1.08); }
}
.breathing {
animation: breathe 2s ease-in-out alternate infinite;
/* Будет плавно увеличиваться и уменьшаться */
}

animation-fill-mode — состояние до и после
/* none: после анимации элемент возвращается к исходным стилям */
.popup { animation: slideIn 0.3s ease; }
/* После 0.3s — снова пропадёт (вернётся к исходным) */
/* forwards: элемент остаётся в конечном состоянии @keyframes */
.appear {
opacity: 0; /* исходно невидимый */
animation: fadeIn 0.5s ease forwards;
/* После анимации: opacity: 1 — остаётся видимым */
}
/* backwards: применить начальное состояние @keyframes сразу */
/* Полезно при animation-delay, чтобы не мелькало */
.delayed {
animation: fadeIn 0.5s ease 1s backwards;
/* Пока идёт задержка (1s) — уже opacity: 0, не мелькает */
}
/* both: forwards + backwards */
.smooth {
animation: slideUp 0.4s ease 0.2s both;
/* Применяет from сразу (нет мелькания при delay) */
/* И остаётся в to после окончания */
}

Готовые анимации для копирования
/* 1. Fade In — появление */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in {
animation: fadeIn 0.5s ease forwards;
}
/* 2. Spin — спиннер загрузки */
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
display: inline-block;
width: 40px;
height: 40px;
border: 4px solid #e5e7eb;
border-top-color: #2563eb;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
/* 3. Pulse — пульсация (уведомления, акценты) */
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.05); opacity: 0.8; }
}
.pulse {
animation: pulse 2s ease-in-out infinite;
}
/* 4. Bounce — прыжок */
@keyframes bounceIn {
0% { transform: scale(0); opacity: 0; }
60% { transform: scale(1.08); opacity: 1; }
80% { transform: scale(0.95); }
100% { transform: scale(1); }
}
.bounce-in {
animation: bounceIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
/* 5. Shake — встряска (ошибки, неверный пароль) */
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 50%, 90% { transform: translateX(-8px); }
30%, 70% { transform: translateX(8px); }
}
.shake {
animation: shake 0.5s ease;
}
/* Запуск: element.classList.add('shake') */
/* Удалить после окончания: */
/* element.addEventListener('animationend', () => el.classList.remove('shake')) */
/* 6. Skeleton shimmer — загрузочный скелет */
@keyframes shimmer {
0% { background-position: -400px 0; }
100% { background-position: 400px 0; }
}
.skeleton {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 37%,
#f0f0f0 63%
);
background-size: 800px 100%;
animation: shimmer 1.4s ease infinite;
border-radius: 4px;
height: 16px;
}
/* 7. Float — парение */
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-12px); }
}
.floating {
animation: float 3s ease-in-out infinite;
}
/* 8. Typing cursor — мигающий курсор */
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.cursor {
display: inline-block;
width: 2px;
height: 1.2em;
background: currentColor;
margin-left: 2px;
animation: blink 1s step-end infinite;
}

Несколько анимаций одновременно
/* Несколько анимаций через запятую */
.element {
animation:
fadeIn 0.5s ease forwards, /* появляется */
float 3s ease-in-out infinite 0.5s; /* потом парит */
}
/* Каскадное появление карточек */
.card { opacity: 0; }
.card:nth-child(1) { animation: fadeIn 0.4s ease 0.0s forwards; }
.card:nth-child(2) { animation: fadeIn 0.4s ease 0.1s forwards; }
.card:nth-child(3) { animation: fadeIn 0.4s ease 0.2s forwards; }
.card:nth-child(4) { animation: fadeIn 0.4s ease 0.3s forwards; }

Управление анимацией через JavaScript
/* animation-play-state: paused / running */
.carousel {
animation: slide 3s linear infinite;
}
/* Пауза при наведении */
.carousel:hover {
animation-play-state: paused;
}
/* Запуск анимации через добавление класса */
.shake-animation {
animation: none; /* по умолчанию нет анимации */
}
.shake-animation.is-shaking {
animation: shake 0.5s ease;
}
// JavaScript
const form = document.querySelector('.shake-animation');
form.addEventListener('animationend', () => {
form.classList.remove('is-shaking'); // убрать класс после окончания
});
function triggerShake() {
form.classList.remove('is-shaking'); // сброс если уже идёт
void form.offsetWidth; // force reflow
form.classList.add('is-shaking');
}

prefers-reduced-motion — уважай пользователей
Некоторые пользователи отключают анимации в настройках ОС — из-за эпилепсии, укачивания или личных предпочтений. Браузер сообщает об этом через медиазапрос prefers-reduced-motion.
/* Отключить все анимации для пользователей, которые этого хотят */
@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;
}
}
/* Более избирательный подход */
@media (prefers-reduced-motion: reduce) {
.spinner { animation: none; } /* убрать вращение */
.floating { animation: none; transform: none; } /* убрать парение */
/* Оставить только критичные: индикаторы загрузки */
}

Часто задаваемые вопросы о CSS animation
Как запустить CSS animation по клику?
Базовый CSS не имеет прямого «запуска по клику», но это легко делается через JavaScript — добавляем класс с анимацией, потом убираем его после окончания. Либо используем состояние :focus или скрытый чекбокс :checked.
/* CSS */
.btn { /* обычная кнопка */ }
.btn.clicked {
animation: pulse 0.3s ease;
}
/* JS */
btn.addEventListener('click', () => {
btn.classList.remove('clicked');
void btn.offsetWidth; // сброс (force reflow)
btn.classList.add('clicked');
});
btn.addEventListener('animationend', () => {
btn.classList.remove('clicked');
});
animation-fill-mode: forwards — что значит?
После окончания анимации элемент остаётся в состоянии последнего кадра (to или 100%). Без forwards элемент возвращается к исходным CSS-стилям — анимация «отматывается». Практически всегда при анимации появления (fadeIn, slideIn) нужен forwards, иначе элемент снова пропадёт.
CSS animation vs JavaScript animation — что быстрее?
CSS animation с transform и opacity выполняется в compositor thread — отдельном потоке, независимо от JavaScript. Это гарантирует 60fps даже если JS занят. JavaScript animation (requestAnimationFrame или GSAP) — гибче и позволяет сложную логику, но потенциально медленнее если не использовать transform. Итог: для простых UI-анимаций — CSS. Для сложных последовательностей, физики, интерактивных сцен — GSAP или Web Animations API.



