~/wiki / motion-i-animatsiya / css-animatsii-chto-umeet-brauzer

CSS анимации без библиотек: что умеет браузер — и это на удивление много

Основной чат

Чат для вайбкодеров: новости, гайды, поиск исполнителей, маркетплейс и разбор реальных кейсов.

$ cd раздел/ $ join vibe dev
CSS анимации без библиотек: что умеет браузер — и это на удивление много - обложка

Основа: transitions и keyframes

Это и так знают все, но стоит освежить — потому что именно на этом строится всё остальное.

Transitions — плавное изменение одного состояния в другое при смене класса или псевдокласса:

css
.button {
  background: #1a1a2e;
  transform: scale(1);
  transition: background 0.2s ease, transform 0.15s ease;
}

.button:hover {
  background: #e94560;
  transform: scale(1.03);
}

Правило которое экономит много времени: анимируй только transform и opacity. Они работают на GPU и не вызывают reflow. top, left, width, height — всё это дорого и заметно на мобильных устройствах.

Keyframes — анимация по ключевым кадрам, запускается автоматически или по классу:

css
@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateY(16px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.card {
  animation: slideUp 0.4s ease forwards;
}

forwards — важный параметр: без него элемент вернётся в исходное состояние после окончания анимации.


@starting-style — появление элементов без вспышки

Раньше если нужно было анимировать появление элемента при добавлении в DOM — это был JavaScript: добавить класс через requestAnimationFrame, иначе браузер не видит разницу начального и конечного состояния.

@starting-style решает это в чистом CSS. Он задаёт стартовые значения свойств в момент когда элемент только появляется на странице:

css
.toast {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.3s ease, transform 0.3s ease,
              display 0.3s allow-discrete;
}

@starting-style {
  .toast {
    opacity: 0;
    transform: translateY(12px);
  }
}

Элемент появляется — анимируется от @starting-style к основным значениям. Плавно, без JS, без setTimeout.

Кроссбраузерный базовый уровень достигнут в 2026 — Safari, Firefox, Chrome поддерживают.


display: none с анимацией выхода

Самая старая боль в CSS: анимировать исчезновение элемента нельзя — display: none отключает его мгновенно, раньше чем переход успевает сыграть. Стандартное решение — setTimeout на длину анимации. Хрупко и ненадёжно.

Теперь работает через transition-behavior: allow-discrete:

css
.dropdown {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.2s ease, transform 0.2s ease,
              display 0.2s allow-discrete;
}

.dropdown[hidden] {
  display: none;
  opacity: 0;
  transform: translateY(-8px);
}

@starting-style {
  .dropdown {
    opacity: 0;
    transform: translateY(-8px);
  }
}

allow-discrete говорит браузеру: дождись окончания перехода, только потом переключай дискретное свойство (display). Работает для тостов, дропдаунов, тултипов — любого элемента который появляется и исчезает.


interpolate-sizeheight: auto наконец анимируется

Аккордеон, раскрывающийся плавно — один из самых частых запросов. Раньше нужно было измерять scrollHeight в JavaScript и анимировать к конкретному значению в пикселях. Ломалось при изменении контента.

Одна строка в :root меняет это:

css
:root {
  interpolate-size: allow-keywords;
}

.panel {
  height: 0;
  overflow: clip;
  transition: height 0.35s ease;
}

.panel[data-open] {
  height: auto;
}

interpolate-size: allow-keywords разрешает браузеру интерполировать к ключевым словам: auto, min-content, fit-content. Это свойство наследуется, поэтому одного объявления на :root достаточно для всего проекта.

Важно: overflow: clip вместо overflow: hiddenclip не создаёт случайный scroll-контейнер который мешает анимации высоты.


Scroll-driven animations — скролл как таймлайн

Это самое мощное из всего что появилось. Анимация прогрессирует не по времени, а по позиции скролла. Никакого JavaScript, никакого IntersectionObserver, никакого GSAP для базовых scroll-эффектов.

Два типа timeline:

scroll() — прогресс зависит от позиции в скролл-контейнере. Идеально для progress bar или parallax-фона:

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

.reading-progress {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 3px;
  background: #e94560;
  transform-origin: left;
  animation: progressBar linear;
  animation-timeline: scroll(root);
}

Полоска прогресса чтения без единой строки JS. Реагирует на скролл плавно, работает на compositor thread.

view() — прогресс зависит от позиции элемента во вьюпорте. Идеально для reveal-эффектов при скролле:

css
@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(24px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.section {
  animation: fadeInUp linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 30%;
}

animation-range: entry 0% entry 30% — анимация играет пока элемент входит во вьюпорт, занимая первые 30% этого входа. После этого остаётся в конечном состоянии.

Поддержка в 2026 — кроссбраузерная. Firefox и Safari закрыли поддержку в 2025–2026, без префиксов.


Scroll-triggered animations — запуск по порогу скролла

В Chrome 145 появилось отдельное от scroll-driven: animation-trigger. Это не таймлайн где анимация скрабится скроллом — это однократный запуск когда элемент пересекает заданный порог. Прощай IntersectionObserver для этого паттерна:

css
@keyframes popIn {
  from { opacity: 0; scale: 0.9; }
  to   { opacity: 1; scale: 1; }
}

.card {
  animation: popIn 0.4s ease forwards paused;
  animation-trigger: view(block 20%);
}

paused — анимация ждёт. animation-trigger: view(block 20%) — запускает когда элемент вошёл на 20% вьюпорта. Один раз, не скрабится.

На момент июня 2026 — Chrome 145+. Firefox и Safari в процессе. Добавляй @supports:

css
@supports (animation-trigger: view()) {
  .card { animation-trigger: view(block 20%); }
}

@property — анимация градиентов и кастомных значений

Градиенты в CSS нельзя анимировать через transition — браузер не понимает как интерполировать между двумя linear-gradient(). Обходной путь был через псевдоэлементы с opacity. @property решает это напрямую, регистрируя кастомное свойство с типом:

css
@property --gradient-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.card {
  background: linear-gradient(var(--gradient-angle), #1a1a2e, #e94560);
  transition: --gradient-angle 0.6s ease;
}

.card:hover {
  --gradient-angle: 180deg;
}

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

@property также открывает анимацию счётчиков, цветовых компонентов и любых числовых CSS-переменных. Поддержка — кроссбраузерная.


View Transitions — переходы между страницами

document.startViewTransition() — один из самых эффектных API которые появились за последние годы. Плавный переход между состояниями DOM или целыми страницами:

javascript
document.startViewTransition(() => {
  updateDOM(); // любое изменение DOM
});

По умолчанию браузер делает crossfade. Кастомизация через CSS:

css
/* Переход между страницами — slide */
::view-transition-old(root) {
  animation: slide-out 0.3s ease forwards;
}
::view-transition-new(root) {
  animation: slide-in 0.3s ease forwards;
}

@keyframes slide-out {
  to { transform: translateX(-100%); }
}
@keyframes slide-in {
  from { transform: translateX(100%); }
}

Для многостраничных сайтов — автоматические переходы через @view-transition { navigation: auto; } в CSS без JavaScript вообще.


prefers-reduced-motion — обязательно

Все анимации выше нужно оборачивать в проверку. Часть пользователей отключает анимации в системных настройках — по медицинским причинам или личным предпочтениям:

css
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}

Одно правило в конце глобального CSS — и все анимации деградируют до мгновенных переходов у тех кто их отключил.


AI помогает: промпты под конкретные задачи

Весь код выше отлично генерируется агентами — CSS хорошо представлен в обучающих данных и новые свойства тоже подхватились. Три промпта под частые задачи:

Reveal-анимации при скролле без JS:

markdown
Добавь анимации появления при скролле для всех элементов
с классом .reveal на странице.

Используй только CSS: animation-timeline: view() и animation-range.
Анимация: opacity 0→1, translateY 20px→0, duration 0.5s ease.
Не использовать JavaScript, IntersectionObserver или сторонние библиотеки.
Добавь @media (prefers-reduced-motion: reduce) с отключением анимаций.

Аккордеон с плавным открытием до height: auto:

markdown
Создай компонент аккордеона на чистом HTML и CSS.
Требования:
- Открытие/закрытие плавно анимирует height до auto без JS-измерений
- Используй interpolate-size: allow-keywords на :root
- Анимация появления контента через @starting-style
- Переключение только через CSS :has() или details/summary без JavaScript
- Поддержка prefers-reduced-motion

Переход между страницами через View Transitions:

markdown
Добавь плавные переходы при навигации между страницами
в Next.js / React Router проекте.

Используй View Transitions API: document.startViewTransition().
Переход: текущая страница уходит влево, новая приходит справа.
CSS для ::view-transition-old и ::view-transition-new.
Добавь fallback: если API не поддерживается — обычная навигация без анимации.
Не использовать Framer Motion или другие библиотеки для этого эффекта.

Когда всё-таки нужна библиотека

CSS не заменяет всё. Три сценария где библиотека оправдана:

Физика и пружины — spring() easing в CSS пока не стандартизирован. Если нужен отскок с реальной физикой — React Spring или GSAP.

Сложные скрабируемые таймлайны — несколько анимаций синхронизированных на одном таймлайне с паузами, повторами и динамическим управлением — GSAP ScrollTrigger.

Интерактивные состояния с жестами — свайп, drag, pinch — это всегда JavaScript. CSS не обрабатывает gesture-события.

Во всём остальном — сначала браузер.

$ cd ../ ← назад к Motion и анимация