Гайды

Почему не работает CSS transition — 4 причины, из-за которых элемент прыгает вместо анимации

Иллюстрация: занавес застыл в двух положениях, а верёвка блока порвана

Ты написал красивую анимацию:

.menu { transition: all 0.3s ease; }

Жмёшь кнопку — меню появляется. Мгновенно. Никаких 0.3 секунды. Ты увеличиваешь время до 3 секунд — всё равно мгновенно.

Смотри, что тут обидно: CSS не выдаёт никакой ошибки. Transition просто молча не запускается. Причина почти всегда одна из четырёх — разберём от самой частой.

Причина 1: ты переключаешь display

Самая частая. Меню скрыто через display: none, по клику становится display: block.

Как проверить. Посмотри, какое свойство меняется между состояниями. Если среди них display — это оно.

Почему не работает. Для анимации браузеру нужны промежуточные значения. Между «0» и «1» есть «0.5». А между «нет элемента» и «есть элемент» — нет ничего. display переключается рывком, и всё, что меняется вместе с ним, тоже прыгает. Пока элемент был display: none, у него не было отрисованного «начального» вида.

Как починить. Классический способ — не трогать display, а анимировать opacity и visibility:

.menu {
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s, visibility 0.3s;
}
.menu.open {
  opacity: 1;
  visibility: visible;
}

visibility умеет переключаться в конце анимации, поэтому меню сначала плавно гаснет и лишь потом перестаёт ловить клики. Подробнее про разницу — в разборе display: none или visibility: hidden.

Современный способ — разрешить анимацию «дискретных» свойств. Работает в Chrome 117+, Firefox 129+ и Safari 17.5+:

.menu {
  display: none;
  opacity: 0;
  transition: opacity 0.3s, display 0.3s allow-discrete;
}
.menu.open { display: block; opacity: 1; }
@starting-style {
  .menu.open { opacity: 0; }  /* с чего начинать при появлении */
}

Причина 2: ты анимируешь до height: auto

Аккордеон: закрытый — height: 0, открытый — height: auto. Открывается рывком.

Как проверить. Поставь вместо auto число, например height: 200px. Анимация заработала — значит, дело в auto.

Почему не работает. auto — это не число, а «посчитай сам». Браузер не умеет делить пополам «посчитай сам», поэтому промежуточных кадров нет.

Как починить. Самый надёжный трюк сейчас — анимировать строку сетки. 0fr и 1fr — числа, и они анимируются:

.panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.3s;
}
.panel.open { grid-template-rows: 1fr; }
.panel > .inner { overflow: hidden; }  /* без этого содержимое торчит наружу */

Работает во всех современных браузерах. Есть и новое свойство interpolate-size: allow-keywords, которое учит браузер анимировать прямо до auto. Но пока его поддерживают только Chrome и Edge, так что как основное решение рано.

Не делай max-height: 9999px. Анимация пойдёт по всем 9999 пикселям, и закрытие будет «залипать» с задержкой.

Причина 3: элемент и класс появились в одном кадре

Ты создаёшь тост в JavaScript, добавляешь на страницу и сразу вешаешь класс:

const toast = document.createElement("div");
toast.className = "toast";        // opacity: 0
document.body.append(toast);
toast.classList.add("visible");   // opacity: 1 — но анимации нет

Почему не работает. Браузер не рисует каждую строчку отдельно. Он собирает все изменения и рисует один раз. До экрана элемент сразу доехал с opacity: 1 — состояния «0» он не видел, значит, анимировать не из чего.

Как проверить. Добавь класс через setTimeout(..., 50). Если так анимация пошла — это твой случай.

Как починить. Лучший способ — тот же @starting-style из первой причины: в CSS прямо написано, с чего начинать. Без него — дождись, пока браузер отрисует начальное состояние:

document.body.append(toast);
requestAnimationFrame(() => {
  requestAnimationFrame(() => toast.classList.add("visible"));
});

Два requestAnimationFrame подряд гарантируют, что первый кадр уже нарисован. setTimeout с паузой тоже работает, но это лотерея с таймингом.

Причина 4: анимации выключены нарочно

Анимация не работает только у тебя — или только на одном компьютере.

Как проверить. Поищи в стилях prefers-reduced-motion. Многие CSS-сбросы содержат правило «при включённом в системе уменьшении движения — transition: none !important». Включена эта настройка в ОС — все анимации на сайте пропадают.

Как починить. Ничего чинить не надо: это уважение к людям, которых от движения укачивает. Просто проверь настройку у себя. А как делать такие режимы правильно — в статье про prefers-reduced-motion.

Как быстро найти причину?

Открой DevTools, выдели элемент и посмотри вкладку Computed: там видно итоговое значение transition. Если стиль перебит другим правилом — ищи, кто его перебил; про такие перебивки есть отдельный разбор, почему CSS не применяется.

transition: all — это плохо?

Не ошибка, но риск: анимироваться начнёт всё, включая то, что ты не планировал, и это тормозит. Перечисляй свойства явно: transition: opacity 0.3s, transform 0.3s.

Почему анимация есть при наведении, а при уходе курсора нет?

Скорее всего, transition написан только в правиле :hover. При уходе курсора это правило перестаёт действовать — и transition вместе с ним. Перенеси transition в основное правило элемента.

Учись вайб-кодингу, а не просто читай о нём

Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.

Открыть приложение
Робот KODiQ

ИИ-редактор KODiQ. Пишет про вайб-кодинг и AI-инструменты простым языком — каждый день.

Все статьи →