Олег Марков
Анимации исчезновения leave-animations в современных веб приложениях
Введение
Анимации исчезновения, или leave-animations, управляют тем, как элементы уводятся из интерфейса в момент их удаления. Если появление элементов уже давно стали оформлять аккуратными входящими анимациями, то про их исчезновение нередко забывают. В итоге пользователь видит резкие скачки интерфейса, а не плавное изменение состояния.
Здесь вы разберете, что такое leave-animations на практике, чем они отличаются от enter- и move-анимаций, какие есть типовые паттерны и как реализовать их на чистом CSS, на JavaScript и внутри популярных библиотек вроде React или Vue. Я буду опираться на реальные сценарии: модальные окна, списки, всплывающие уведомления.
Цель статьи — чтобы вы могли уверенно проектировать и реализовывать анимации исчезновения, не ломая логику приложения, не создавая утечек памяти и не ухудшая производительность.
Концепция leave-animations
Что такое анимации исчезновения
Анимация исчезновения — это переходное состояние элемента между моментом, когда он еще виден на экране, и моментом, когда его уже нет в DOM или он полностью скрыт. Ключевая особенность: элемент логически уже должен уйти, но физически задерживается на экране для проигрывания анимации.
Условно есть три этапа жизни визуального элемента:
- Enter — появление элемента
- Active — нормальное состояние на экране
- Leave — уход и исчезновение элемента
В leave-состоянии:
- бизнес-логика может считать элемент удаленным
- пользователь все еще видит его на экране
- система должна аккуратно синхронизировать визуальное состояние с логическим
Чем leave-анимации отличаются от enter-анимаций
Можно представить, что enter и leave — зеркальные процессы, но есть несколько важных отличий:
- В момент enter DOM-узел уже создан и вам нужно лишь показать его красиво.
- В момент leave DOM-узел чаще всего хотят удалить как можно быстрее, но вам нужно отложить это удаление, пока не завершится анимация.
- При enter вы анимируете «появление» (прозрачность с 0 до 1, масштаб с 0.9 до 1 и т. д.).
- При leave вы анимируете «исчезновение» (прозрачность с 1 до 0, сдвиг за пределы экрана, уменьшение масштаба и так далее).
Отсюда рождаются типичные технические задачи:
- как не удалить элемент слишком рано
- как не забыть удалить его после анимации
- как не оставить «мертвые» обработчики и таймеры
Типовые сценарии использования
Посмотрите на несколько распространенных случаев, где leave-animations особенно полезны:
- модальные окна
- всплывающие уведомления (toasts)
- списки с удалением элементов (корзина, список задач)
- выпадающие списки и меню
- оверлеи и затемнения фона
- слайдеры и карусели
В каждом из этих сценариев важно, чтобы исчезновение элемента не выглядело резким или неожиданным, и чтобы окружающий интерфейс вел себя предсказуемо.
Базовые паттерны реализации
CSS transition на классах
Проще всего реализовать анимацию исчезновения с помощью CSS переходов и смены классов. Давайте разберемся на примере всплывающего уведомления.
HTML:
<div class="toast toast-visible" id="toast">
Сообщение отправлено
<button class="toast-close" id="toastClose">×</button>
</div>
CSS:
.toast {
opacity: 0; /* Базовое состояние - невидимо */
transform: translateY(8px); /* Слегка смещено вниз */
transition: opacity 0.25s ease, transform 0.25s ease; /* Анимация свойств */
}
.toast-visible {
opacity: 1; /* Видимое состояние */
transform: translateY(0); /* На своем месте */
}
.toast-leave {
opacity: 0; /* Исчезновение */
transform: translateY(-8px);/* Сдвиг вверх при уходе */
}
JavaScript:
const toast = document.getElementById("toast");
const closeBtn = document.getElementById("toastClose");
closeBtn.addEventListener("click", () => {
// Добавляем класс ухода
toast.classList.add("toast-leave");
toast.classList.remove("toast-visible");
// Ждем окончания transition, чтобы удалить элемент
const handleTransitionEnd = (event) => {
// Проверяем что завершилась анимация opacity, а не другой transition
if (event.propertyName === "opacity") {
toast.remove(); // Полностью удаляем элемент из DOM
toast.removeEventListener("transitionend", handleTransitionEnd);
}
};
toast.addEventListener("transitionend", handleTransitionEnd);
});
Обратите внимание, как здесь решены ключевые моменты:
- для начала анимации мы меняем классы
- для удаления дожидаемся события transitionend
- отписываемся от обработчика, чтобы не допустить утечек
CSS keyframes и классы состояний
Иногда удобнее использовать keyframes-анимации, особенно если поведение сложнее, чем простое изменение пары свойств. Смотрите, как это можно сделать.
CSS:
@keyframes fadeOutScale {
from {
opacity: 1; /* Начинаем с полностью видимого элемента */
transform: scale(1);
}
to {
opacity: 0; /* Заканчиваем нулевой прозрачностью */
transform: scale(0.9); /* Немного уменьшаем элемент */
}
}
.modal {
opacity: 1; /* Видимо по умолчанию */
transform: scale(1);
}
.modal-leave {
animation: fadeOutScale 0.3s ease forwards;
/* forwards - сохраняем финальное состояние анимации */
}
JavaScript:
const modal = document.querySelector(".modal");
const closeButton = document.querySelector(".modal-close");
closeButton.addEventListener("click", () => {
// Запускаем leave-анимацию
modal.classList.add("modal-leave");
const handleAnimationEnd = () => {
modal.remove(); // Удаляем модалку после завершения анимации
modal.removeEventListener("animationend", handleAnimationEnd);
};
modal.addEventListener("animationend", handleAnimationEnd);
});
Keyframes хорошо подходят, когда вам нужно:
- использовать сложные траектории
- комбинировать несколько фаз анимации
- контролировать промежуточные состояния
Управление жизненным циклом элемента
Отложенное удаление элемента
Ключевая техника для leave-animations — отложенное удаление. С точки зрения логики приложение уже решило, что элемент «больше не нужен», но вы физически удаляете его позже.
Алгоритм можно описать так:
- Поймать событие, которое говорит «элемент должен исчезнуть».
- Перевести элемент в leave-состояние (через класс или атрибут).
- Дождаться окончания анимации (transitionend или animationend).
- Удалить элемент из DOM.
- Освободить связанные ресурсы (обработчики, таймеры).
Смотрите простой шаблон:
function withLeaveAnimation(element, leaveClass, removeCallback) {
// Добавляем класс leave к элементу
element.classList.add(leaveClass);
const handleEnd = (event) => {
// Для надежности можно не проверять propertyName, если анимация одна
element.removeEventListener("transitionend", handleEnd);
element.removeEventListener("animationend", handleEnd);
// Удаляем элемент
if (typeof removeCallback === "function") {
// Даём возможность внешнему коду решить как удалить элемент
removeCallback(element);
} else {
element.remove();
}
};
// Поддерживаем и transition, и animation
element.addEventListener("transitionend", handleEnd);
element.addEventListener("animationend", handleEnd);
}
Такую функцию можно использовать повторно для разных компонентов, не дублируя логику ожидания завершения анимации.
Состояние «логически удален, визуально еще виден»
Это важное понятие, которое часто упускают. Представим список задач, где вы удаляете элемент.
- Пользователь нажимает «Удалить».
- Задача убирается из данных (массив задач).
- Элемент в интерфейсе начинает анимированно исчезать.
С точки зрения данных задачи уже нет, но DOM-элемент все еще жив. Это нормально, но важно:
- не пытаться обновлять этот элемент при следующих рендерах
- не держать на нем критичную бизнес-логику
- не опираться на него в вычислениях состояния
Решить это можно двумя способами:
- Удалять задачу из данных только после завершения анимации.
- Помечать задачу флагом deleted и не показывать ее в новых рендерах, а отдельный слой логики отвечает за отложенное удаление DOM-узла.
Во фреймворках обычно используется второй вариант, потому что он лучше уживается с виртуальным DOM и реактивностью.
Реализация на чистом CSS и JavaScript
Пример: анимация исчезновения элемента списка
Давайте реализуем список, где при удалении элемента он плавно схлопывается, а остальные элементы «подъезжают» вверх.
HTML:
<ul id="todoList">
<li class="item">
Купить хлеб
<button class="remove">Удалить</button>
</li>
<li class="item">
Написать статью
<button class="remove">Удалить</button>
</li>
</ul>
CSS:
.item {
overflow: hidden; /* Нужно чтобы высота схлопывалась красиво */
transition:
opacity 0.25s ease,
transform 0.25s ease,
height 0.25s ease,
margin 0.25s ease;
opacity: 1;
transform: translateX(0);
height: 40px; /* Фиксированная высота строки */
margin: 4px 0;
}
.item-leave {
opacity: 0; /* Исчезаем по прозрачности */
transform: translateX(16px); /* Сдвигаемся вправо */
height: 0; /* Высота схлопывается */
margin: 0; /* Убираем отступы */
padding-top: 0;
padding-bottom: 0;
}
JavaScript:
const list = document.getElementById("todoList");
list.addEventListener("click", (event) => {
// Проверяем клик по кнопке удаления
if (event.target.matches(".remove")) {
const item = event.target.closest(".item");
// Добавляем класс ухода
item.classList.add("item-leave");
const handleEnd = (event) => {
// Убедимся что завершился transition по height
if (event.propertyName === "height") {
item.remove(); // Удаляем элемент после схлопывания
item.removeEventListener("transitionend", handleEnd);
}
};
item.addEventListener("transitionend", handleEnd);
}
});
Здесь вы видите, как анимация решает сразу две задачи:
- визуально показывает удаление элемента
- аккуратно освобождает место в списке, не создавая резких скачков
Пример: фейд-аут оверлея и модального окна
Частый сценарий: есть затемнение фона и модальное окно поверх. При закрытии важно:
- сначала плавно убрать модальное окно
- затем убрать затемнение
- удалить оба элемента
CSS:
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
opacity: 1;
transition: opacity 0.2s ease;
}
.overlay-leave {
opacity: 0; /* Оверлей постепенно исчезает */
}
.modal {
position: fixed;
top: 50%;
left: 50%;
transform:
translate(-50%, -50%)
scale(1);
opacity: 1;
transition:
opacity 0.2s ease,
transform 0.2s ease;
}
.modal-leave {
opacity: 0; /* Модалка становится прозрачной */
transform:
translate(-50%, -50%)
scale(0.95); /* Легкое уменьшение увеличивает ощущение «ухода» */
}
JavaScript:
const overlay = document.querySelector(".overlay");
const modal = document.querySelector(".modal");
const closeButton = document.querySelector(".modal-close");
function closeModal() {
// Переводим оба элемента в leave-состояние
overlay.classList.add("overlay-leave");
modal.classList.add("modal-leave");
let overlayDone = false;
let modalDone = false;
const tryRemove = () => {
// Удаляем после окончания обеих анимаций
if (overlayDone && modalDone) {
overlay.remove();
modal.remove();
}
};
overlay.addEventListener("transitionend", () => {
overlayDone = true;
tryRemove();
}, { once: true });
modal.addEventListener("transitionend", (event) => {
if (event.propertyName === "opacity") {
modalDone = true;
tryRemove();
}
}, { once: true });
}
closeButton.addEventListener("click", closeModal);
overlay.addEventListener("click", (event) => {
// Закрываем по клику вне модалки
if (event.target === overlay) {
closeModal();
}
});
Здесь я показываю, как синхронизировать несколько leave-анимаций и не удалять элементы, пока обе не завершены.
Leave-animations во фреймворках
Vue Transition и leave-classes
Vue предлагает встроенный механизм для enter/leave-анимаций. Смотрите простой пример:
<template>
<transition name="fade">
<div v-if="visible" class="alert">
Сообщение
<button @click="visible = false">Закрыть</button>
</div>
</transition>
</template>
CSS:
/* Начальное состояние при появлении */
.fade-enter-from {
opacity: 0;
transform: translateY(8px);
}
/* Активное состояние анимации появления */
.fade-enter-active {
transition: opacity 0.2s ease, transform 0.2s ease;
}
/* Конечное состояние появления */
.fade-enter-to {
opacity: 1;
transform: translateY(0);
}
/* Начальное состояние при исчезновении */
.fade-leave-from {
opacity: 1;
transform: translateY(0);
}
/* Активное состояние анимации исчезновения */
.fade-leave-active {
transition: opacity 0.2s ease, transform 0.2s ease;
}
/* Конечное состояние исчезновения */
.fade-leave-to {
opacity: 0;
transform: translateY(-8px);
}
Vue автоматически:
- добавит классы fade-leave-from и fade-leave-active при скрытии элемента
- сменит fade-leave-from на fade-leave-to на следующем кадре
- дождется завершения transition
- удалит элемент из DOM
Вам не нужно вручную ждать события transitionend. Это снимает много технических забот.
React и анимации исчезновения
В React классический подход — использовать библиотеку react-transition-group. Она добавляет простую обертку над жизненным циклом элементов.
Смотрите, как это выглядит:
import { CSSTransition, TransitionGroup } from "react-transition-group";
function TodoList({ items, onRemove }) {
return (
<TransitionGroup component="ul" className="todo-list">
{items.map((item) => (
<CSSTransition
key={item.id}
timeout={250}
classNames="todo"
>
<li className="todo-item">
{item.text}
<button onClick={() => onRemove(item.id)}>Удалить</button>
</li>
</CSSTransition>
))}
</TransitionGroup>
);
}
CSS:
/* Появление */
.todo-enter {
opacity: 0;
transform: translateY(8px);
}
.todo-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 0.25s ease, transform 0.25s ease;
}
/* Исчезновение */
.todo-exit {
opacity: 1;
transform: translateY(0);
}
.todo-exit-active {
opacity: 0;
transform: translateY(-8px);
transition: opacity 0.25s ease, transform 0.25s ease;
}
CSSTransition:
- отслеживает монтирование и размонтирование дочернего элемента
- добавляет классы todo-exit и todo-exit-active
- ждет завершения анимации по timeout или событию
- удаляет элемент из DOM
Здесь логическое удаление (из массива items) и визуальное исчезновение синхронизируются автоматически.
Производительность и «дружелюбные» анимации
Какие свойства анимировать, а какие лучше не трогать
Для производительных leave-анимаций важно выбирать свойства, которые браузер умеет анимировать на уровне композитинга, без перерасчета layout.
Рекомендуемые свойства:
- opacity
- transform (translate, scale, rotate)
- filter (аккуратно, может быть тяжелым на слабых устройствах)
Нежелательные:
- top, left, right, bottom
- width, height
- margin, padding
- box-shadow (особенно большие и размытые)
Если вам нужно визуально «схлопнуть» элемент по высоте, как в примере со списком, это уже компромисс. Здесь полезно:
- ограничивать количество одновременно анимируемых элементов
- сокращать длительность анимации
- не запускать такие анимации при массовых изменениях списка
Анимации и восприятие пользователя
Существует несколько практических рекомендаций, которые стоит учитывать:
- Длительность большинства leave-анимаций — 150–300 мс.
- Анимация исчезновения обычно чуть быстрее, чем появления.
- Исчезновение редких или важных элементов можно сделать чуть заметнее (комбинация фейда и небольшого движения).
- Массовые удаление лучше анимировать проще и быстрее.
Старайтесь, чтобы анимация не мешала реальному действию пользователя. Например:
- если пользователь закрывает уведомление, не задерживайте его интерфейсом, который «умирает» полсекунды;
- если он удаляет элементы пачками, не запускайте длинные последовательные анимации, которые он будет ждать.
Сложные кейсы и нюансы
Прерывание анимации и повторные действия
Представьте ситуацию:
- элемент начал leave-анимацию
- пользователь передумал и пытается «вернуть» элемент
- или код повторно пытается удалить его
Здесь полезно предусмотреть несколько механизмов:
- Флаг состояния
let isLeaving = false;
function startLeave(element) {
if (isLeaving) return; // Не запускаем повторно
isLeaving = true;
element.classList.add("item-leave");
element.addEventListener("transitionend", () => {
element.remove();
isLeaving = false;
}, { once: true });
}
- Отмена leave при повторном действии
Можно реализовать логику «мягкого удаления» с возможностью отмены:
- при первом клике запускается leave-анимация
- в течение небольшого промежутка времени можно нажать «Вернуть»
- анимация разворачивается в обратную сторону
Технически это можно сделать через:
- отдельный класс item-leave-cancel
- или повторный расчет стилей через JavaScript
Синхронизация нескольких элементов
Иногда нужно анимировать набор связанных элементов одновременно, например:
- строку в таблице и связанные с ней подсказки
- карточку и тень под ней
- вложенные блоки
Главный принцип: решите, какой элемент является «ведущим», и синхронизируйте удаление остальных с его анимацией.
Подход:
- назначите событие transitionend или animationend только на ведущий элемент
- внутри обработчика удалите все связанные узлы
- не вешайте обработчики на каждый отдельный блок
Это снижает вероятность рассинхронизации, если какая-то анимация не сработает.
Обработка situation «анимация не сработала»
Иногда событие transitionend или animationend может не прийти:
- если элемент стал display none до завершения
- если анимация была отключена пользователем через системные настройки
- если класс был переключен слишком быстро
Полезно добавлять защитный таймаут:
function safelyRemoveWithAnimation(element, leaveClass, duration) {
let ended = false;
const done = () => {
if (ended) return;
ended = true;
element.remove();
element.removeEventListener("transitionend", onEnd);
element.removeEventListener("animationend", onEnd);
};
const onEnd = () => {
done();
};
element.classList.add(leaveClass);
element.addEventListener("transitionend", onEnd);
element.addEventListener("animationend", onEnd);
// Таймаут немного больше, чем ожидаемая длительность
setTimeout(done, duration + 50);
}
Так вы гарантируете удаление элемента даже в нестандартных ситуациях.
Инструменты и библиотеки
Микробиблиотеки для анимированного удаления
Помимо крупных фреймворков есть небольшие утилиты, которые помогают добавить leave-animations в «ванильный» проект.
Типичные возможности таких библиотек:
- добавление/удаление классов с учетом анимаций
- ожидание завершения анимации с fallback-таймаутами
- плагины для конкретных паттернов (модалки, тосты, аккордеоны)
Даже если вы не используете стороннюю библиотеку, полезно посмотреть на их API как на источник идей, как аккуратно оформить свою обертку над анимациями.
DevTools и отладка анимаций
Браузерные DevTools (Chrome, Firefox) позволяют:
- просматривать активные анимации
- замедлять анимации в несколько раз
- видеть график изменения свойств
Рекомендация:
- во время разработки leave-animations включайте замедление анимаций в 4–5 раз
- проверяйте, что классы применяются в нужные моменты
- следите, что обработчики событий не остаются на удаленных элементах
Заключение
Анимации исчезновения — это не просто визуальный эффект. Это часть жизненного цикла интерфейсных элементов, которая должна быть согласована с логикой приложения, управлением DOM и ожиданиями пользователя.
Ключевые идеи, которые важно вынести:
- элемент может быть логически удален, но визуально еще присутствовать на экране
- для leave-animations практически всегда требуется отложенное удаление DOM-узла
- надежная реализация опирается на события transitionend и animationend плюс защитные таймауты
- фреймворки (Vue, React, другие) уже дают готовые абстракции для работы с enter/leave-анимациями
- производительность зависит от того, какие свойства вы анимируете и сколько элементов задействовано одновременно
Используя подходы, о которых вы прочитали, вы можете постепенно внедрять анимации исчезновения в свой интерфейс: начинать с простых fade-out для уведомлений и модалок, а затем двигаться к более сложным сценариям со списками и связанными компонентами.
Частозадаваемые технические вопросы
Как сделать так, чтобы при leave-анимации списка соседние элементы плавно сдвигались, а не прыгали
Используйте переходы не только на удаляемом элементе, но и на контейнере или соседних элементах. В простых случаях достаточно задать transition для margin и height у элементов списка. Для более плавных эффектов изучите техники FLIP-анимаций (First Last Invert Play) — там рассчитывается разница позиций элементов до и после изменения, и эта разница анимируется через transform.
Как отключать или упрощать leave-анимации для пользователей с настройкой уменьшения движения
Проверьте медиавыражение prefers-reduced-motion. В CSS можно написать:
@media (prefers-reduced-motion: reduce) {
.toast,
.modal,
.item {
transition: none;
animation: none;
}
}
А в JavaScript, если нужно, можно сокращать duration и не дожидаться окончания анимации: сразу удалять элемент, если пользователь выбрал минимальное движение.
Как синхронизировать leave-анимацию с запросами на сервер (например при удалении записи в базе)
Обычно удобно использовать два шага:
- Сначала отправить запрос на сервер и дождаться успешного ответа.
- После успеха запускаем leave-анимацию и удаляем элемент из локального списка.
Если важно мгновенное визуальное удаление, можно делать оптимистичный UI: запускать leave-анимацию сразу, а при ошибке запроса показывать отдельное уведомление и восстанавливать элемент (enter-анимацией) при необходимости.
Что делать если элемент может быть удален другим кодом пока идет leave-анимация
Введите централизованный метод удаления. Например удаление элемента из DOM всегда происходит только через функцию removeWithAnimation. Любые попытки удалить элемент напрямую замените вызовом этой функции. Внутри функции учитывайте состояние isLeaving, чтобы не запускать анимацию повторно и не удалять один и тот же элемент дважды.
Как тестировать логику leave-анимаций в автоматических тестах
В юнит-тестах обычно не полагаются на реальные transitionend и animationend. Вместо этого:
- выносите логику в отдельные функции, куда можно передать «фейковый» callback окончания анимации
- или усиливайте проверку за счет явного вызова «завершения» анимации в тесте Можно также настроить в тестовой среде нулевую длительность transition и эмулировать событие окончания анимации вручную, чтобы не замедлять тесты реальным ожиданием времени.
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев