animation-fill-mode решает, что видно ДО старта анимации (во время animation-delay) и что остаётся ПОСЛЕ её окончания. По умолчанию (none) элемент до и после анимации выглядит как обычно, без изменений. forwards сохраняет конечный вид навсегда, backwards показывает начальный вид уже во время задержки, both объединяет оба эффекта.
Попробуй прямо сейчас: тост появляется снизу и ОСТАЁТСЯ на экране - это работа animation-fill-mode: forwards, который держит конечное состояние (opacity:1, без сдвига) даже после того, как сама анимация технически закончилась. Поменяй forwards на none (или удали строку совсем) - через полсекунды тост СНОВА исчезнет, несмотря на то что визуально уже полностью появился, потому что без forwards браузер возвращает элемент к его обычному (невидимому) стилю.
До начала: none и backwards
Если у анимации есть задержка (animation-delay), по умолчанию (none) элемент всё это время выглядит как обычно - его собственный, неанимированный стиль. backwards меняет это: элемент СРАЗУ показывает вычисленные значения ПЕРВОГО keyframe, ещё до того, как задержка закончится и анимация реально начнётся. Какой keyframe считается "первым", зависит от animation-direction: при normal и alternate это 0%/from, а при reverse и alternate-reverse - наоборот, 100%/to.
.badge {
animation-name: pulse;
animation-duration: 1s;
animation-delay: 2s;
animation-fill-mode: backwards;
} - все 2 секунды задержки элемент уже выглядит так, как задано на 0% в @keyframes pulse, без "вспышки" обычного вида.
Без backwards будет "вспышка" перед стартом:
.badge {
animation-name: pulse;
animation-duration: 1s;
animation-delay: 2s;
} - все 2 секунды задержки элемент показывает свой обычный стиль, и только потом резко переключается на вид 0% - если они заметно отличаются, это выглядит как рывок.
После окончания: none и forwards
После того как анимация доиграла (или если animation-iteration-count не infinite), по умолчанию (none) элемент возвращается к своему обычному стилю - "как будто анимации и не было". forwards сохраняет вычисленные значения ПОСЛЕДНЕГО пройденного keyframe навсегда. Неочевидный момент, подтверждённый собственной проверкой: "последний" keyframe зависит не только от направления, но и от ЧЁТНОСТИ числа повторов - с animation-direction: alternate и ЧЁТНЫМ animation-iteration-count (например 2) forwards зафиксирует состояние 0%, а НЕ 100%, потому что последний (чётный) проход шёл в обратную сторону.
Есть и более коварная ловушка: forwards бессилен, если сама анимация назначена ТОЛЬКО внутри правила :hover. Пока курсор наведён, forwards честно держит конечный вид - но как только курсор уходит, правило .el:hover перестаёт действовать, и анимация СНИМАЕТСЯ ЦЕЛИКОМ вместе со своим forwards-состоянием - элемент мгновенно прыгает к базовому стилю. Это та же по сути проблема, что уже разбиралась для transition и :hover в уроке 06: если эффект должен пережить уход курсора, саму анимацию нужно вешать на БАЗОВЫЙ селектор (управляя её запуском, например, через смену имени или добавление класса через JS), а не только на :hover.
.spinner {
animation-name: appear;
animation-duration: .5s;
animation-fill-mode: forwards;
} - конечный вид сохраняется навсегда, пока сама анимация не будет убрана из каскада.
Неправильное ожидание - что forwards переживёт снятие правила :hover:
.card:hover {
animation-name: appear;
animation-duration: .5s;
animation-fill-mode: forwards;
} - при уходе курсора .card:hover перестаёт совпадать, анимация снимается целиком, forwards ничего не спасает.
Частые ошибки новичков
| Ошибка | Что происходит | Как исправить |
|---|---|---|
| Не указать animation-fill-mode вообще и удивляться "прыжку" обратно | По умолчанию none - элемент возвращается к обычному стилю сразу после конца анимации | Указать animation-fill-mode: forwards, если конечный вид должен остаться |
| Ожидать, что forwards сработает, если анимация назначена только на :hover | При уходе курсора правило :hover перестаёт действовать, и анимация снимается целиком вместе с forwards-состоянием | Вешать саму анимацию на базовый селектор, а не только на :hover (как и с transition в уроке 06) |
| Считать, что forwards с alternate всегда фиксирует состояние 100% | С чётным animation-iteration-count последний проход идёт НАЗАД, и фиксируется состояние 0% | Проверять чётность числа повторов при использовании alternate вместе с forwards |
| Использовать animation-delay без backwards и получать "вспышку" исходного вида | Все секунды задержки элемент показывает обычный стиль, а не вид 0% из @keyframes | Добавить animation-fill-mode: backwards (или both), если нужно избежать вспышки |
Шпаргалка
| Запись | Что означает |
|---|---|
| animation-fill-mode: none; (по умолчанию) | До и после анимации - обычный стиль элемента |
| animation-fill-mode: forwards; | После окончания - сохраняется вид последнего пройденного keyframe |
| animation-fill-mode: backwards; | Во время задержки - уже виден вид первого keyframe |
| animation-fill-mode: both; | Объединяет backwards (до) и forwards (после) |
| forwards + alternate + чётное число повторов | Фиксируется состояние 0%, а не 100% |
| forwards только на :hover | Не спасает - при уходе курсора анимация снимается целиком |
Коротко: отвечаем на вопросы
Как сделать так, чтобы после анимации элемент остался в конечном виде, а не возвращался к исходному?
Реальный ответ с Хабра - добавить animation-fill-mode: forwards. Это заставляет браузер сохранять вычисленные значения последнего пройденного keyframe навсегда (пока сама анимация не будет убрана из каскада), вместо того чтобы возвращать элемент к обычному, неанимированному стилю сразу после окончания.
Почему не работает animation-fill-mode: forwards - анимация доигрывает до конца, но потом всё равно возвращается назад?
Частая причина, встречающаяся в реальных вопросах разработчиков - анимация назначена ТОЛЬКО внутри правила .el:hover. Пока курсор наведён, forwards честно держит конечный вид, но как только курсор уходит, .el:hover перестаёт совпадать, и анимация снимается из каскада ЦЕЛИКОМ вместе с её forwards-состоянием - элемент мгновенно прыгает к базовому стилю. Собственная проверка подтвердила: сразу после ухода курсора число активных анимаций у элемента становится равным нулю. Решение - вешать саму анимацию на базовый селектор, а не только на :hover, точно так же, как и для transition.