Angular Animations in 2026: animate.enter, animate.leave and CSS [2026]
If you learned Angular animations a few years ago, you learned a small language of its own: trigger(), state(), transition(), animate(), a provider in the app config and an @ binding in the template. That API still runs, but it is no longer where Angular is heading. The @angular/animations package is deprecated as of v20.2, and the same release added animate.enter and animate.leave, which let plain CSS handle the one thing CSS couldn't do by itself: animating an element as Angular adds it to or removes it from the DOM.
This is lesson 13.1 of the Angular Tutorial, and the first lesson of Module 13 on animations. It follows lesson 12.5, which closed the AI module. You'll learn which tools a modern Angular app uses for each kind of animation, how animate.enter and animate.leave work, what setup they need (almost none), and how to treat existing code built on @angular/animations, including how to migrate it.
The modern toolbox #
Animations in an Angular app fall into four jobs, and each now has a native answer:
| Job | Modern approach | Legacy equivalent |
|---|---|---|
| Animate a state change on an element that stays in the DOM (open/closed, active, expanded) | CSS transition or @keyframes, with a class bound to a signal |
trigger() + state() + transition('open <=> closed') |
| Animate an element as it's added | animate.enter="class" + CSS |
transition(':enter', ...) |
| Animate an element as it's removed | animate.leave="class" + CSS, or (animate.leave) with a function |
transition(':leave', ...) |
| Animate between routes | Router view transitions with withViewTransitions() (lesson 13.3) |
Route animations with query() and group() |
What's no longer needed: a provider, a separate package, an Angular-specific animation DSL, and the runtime that interpreted it. The browser runs the animations, which also lets them run on the compositor thread where possible.
Why the change #
The Angular team's migration guide gives two concrete reasons. Dropping @angular/animations can significantly reduce bundle size, because its runtime is no longer shipped. And native CSS animations can benefit from hardware acceleration, which an animation engine running in JavaScript can't always use. CSS has also caught up with most of what the old DSL offered: @starting-style for entry styles, transitions on discrete properties, calc-size() for animating to auto heights in supporting browsers, and custom properties for staggering.
The gap that remained was removal. CSS can't animate an element that Angular has already taken out of the DOM. animate.leave closes that gap by telling Angular to wait for the animation before removing the element.
Setup #
There is nothing to install or provide. animate.enter and animate.leave are not directives you import; they are syntax the Angular compiler supports directly, available in any component template. You don't need @angular/animations in package.json, and you don't need provideAnimations() or provideAnimationsAsync() in app.config.ts for them.
A minimal example:
@Component({
selector: 'app-notice',
template: `
<button type="button" (click)="open.set(!open())">Toggle notice</button>
@if (open()) {
<p class="notice" animate.enter="notice-in" animate.leave="notice-out">
Your changes were saved.
</p>
}
`,
styleUrl: './notice.css',
})
export class Notice {
open = signal(false);
}
/* notice.css */
.notice-in {
animation: notice-in 250ms ease-out;
}
@keyframes notice-in {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
.notice-out {
opacity: 0;
transform: translateY(-8px);
transition: opacity 200ms ease-in, transform 200ms ease-in;
}
Because the classes are applied to elements in this component's own template, normal component styles (with view encapsulation) apply to them. No global stylesheet is needed.
How animate.enter works #
When Angular inserts an element that has animate.enter, it adds the class (or classes) you named. The CSS animation or transition on that class runs. When the animation finishes, Angular removes the class again, so animation classes are only present while the animation is active.
Two details matter:
- The longest animation wins. If the class sets several animations or transitions with different durations, Angular removes the classes only after the longest one has completed.
- A keyframe animation is the simplest entry. It defines its own starting point (
from). If you'd rather use atransition, you need to give the browser a "before" style; the CSS@starting-stylerule does that.
/* transition-based entry, defined on the element itself */
.card {
opacity: 1;
transition: opacity 200ms ease-out;
@starting-style {
opacity: 0;
}
}
You can pass a class string, several classes separated by spaces, or bind an expression:
<div animate.enter="fade-in slide-up">…</div>
<div [animate.enter]="enterClass()">…</div>
The binding form is useful when the animation depends on state, such as sliding in from the left or right depending on navigation direction.
How animate.leave works #
animate.leave reverses the order. When the element is about to be removed, for example because an @if turned false or an item left a @for list, Angular adds the leave class, waits for its animations to complete, and only then removes the element.
@for (toast of toasts(); track toast.id) {
<div class="toast" animate.enter="toast-in" animate.leave="toast-out">{{ toast.text }}</div>
}
If animate.leave sits on a descendant of the element being removed, within the same component template, the descendant's leave animation runs before the parent is removed.
For animations written in JavaScript, use the event form. Angular passes an AnimationCallbackEvent with the element as target and an animationComplete() function you must call when you're done:
import { AnimationCallbackEvent, Component, signal } from '@angular/core';
@Component({
selector: 'app-panel',
template: `
@if (open()) {
<section (animate.leave)="slideAway($event)">…</section>
}
`,
})
export class Panel {
open = signal(true);
slideAway(event: AnimationCallbackEvent) {
const el = event.target as HTMLElement;
const anim = el.animate(
[{ transform: 'translateX(0)' }, { transform: 'translateX(100%)' }],
{ duration: 250, easing: 'ease-in' },
);
anim.finished.then(() => event.animationComplete());
}
}
This is also how you integrate third-party animation libraries: run the library's animation on event.target and call animationComplete() in its completion callback. If you never call it, Angular removes the element automatically after four seconds; the MAX_ANIMATION_TIMEOUT token (in milliseconds) changes that limit.
Animating state changes without enter or leave #
Most animations in an app aren't enter or leave animations at all. A sidebar expanding, a chevron rotating, a row highlighting: the element stays in the DOM and only its state changes. For those, bind a class to a signal and let CSS transition between the two states:
<button type="button" (click)="expanded.set(!expanded())" [attr.aria-expanded]="expanded()">
Details <span class="chevron" [class.open]="expanded()"></span>
</button>
.chevron {
display: inline-block;
transition: transform 150ms ease-out;
}
.chevron.open {
transform: rotate(90deg);
}
No Angular animation API is involved. Lesson 13.2 builds on this with keyframes, staggering and more complex state animations.
Existing code: @angular/animations #
You'll still meet the legacy API in older codebases, and it continues to work while deprecated. It looks like this:
// app.config.ts
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
export const appConfig: ApplicationConfig = {
providers: [provideAnimationsAsync()],
};
import { trigger, state, style, transition, animate } from '@angular/animations';
@Component({
selector: 'app-legacy-panel',
animations: [
trigger('openClose', [
state('open', style({ height: '*', opacity: 1 })),
state('closed', style({ height: '0', opacity: 0 })),
transition('open <=> closed', animate('200ms ease-out')),
]),
],
template: `<div [@openClose]="isOpen() ? 'open' : 'closed'">…</div>`,
})
export class LegacyPanel {
isOpen = signal(false);
}
How to treat it:
| Situation | Recommendation |
|---|---|
| New components | Use CSS, animate.enter and animate.leave; don't add new @angular/animations code |
| Existing component with simple enter/leave or state animations | Migrate when you next touch it; these map directly to CSS |
Heavy use of query(), group(), stagger() and route animations |
Plan the migration; most map to CSS selectors, delays and view transitions (13.2, 13.3) |
| Library you don't control uses it | Keep the provider until the library migrates |
One rule matters during migration: you can't mix legacy animations and animate.enter/animate.leave within the same component. Migrate a component as a whole. The guide also notes that content projection between legacy and new animations isn't supported, so test projected content carefully when parents and children are on different systems.
The migration patterns, covered in detail in the next lessons:
| Legacy | Replace with |
|---|---|
state() + transition() between named states |
CSS classes bound with [class.x]="signal()" plus transition |
transition(':enter') / transition(':leave') |
animate.enter / animate.leave |
stagger() |
animation-delay / transition-delay using a --index custom property |
query() for child elements |
Ordinary CSS selectors on child elements |
AnimationPlayer |
element.getAnimations() and the Web Animations API |
| Route animations | withViewTransitions() and ::view-transition-* CSS |
When the last legacy animation is gone, remove the provideAnimations()/provideAnimationsAsync() call and uninstall @angular/animations.
Gotchas #
| Symptom | Cause | Fix |
|---|---|---|
Element disappears instantly despite animate.leave |
The leave class has no animation or transition | Give the class a real transition or animation |
| Entry transition doesn't play | transition with no "before" state |
Use a keyframe animation, or add @starting-style |
| Element lingers about four seconds before vanishing | (animate.leave) handler never calls animationComplete() |
Call it when the animation finishes |
| Animations break in a half-migrated component | Legacy [@trigger] bindings and animate.enter/animate.leave in one component, which isn't supported |
Migrate the whole component at once |
| Styles for the animation class don't apply | Class defined in another component's stylesheet | Define it in the component whose template uses it |
| Animations don't run in unit tests | TestBed disables them by default |
Expected; see lesson 13.4 for enabling them |
What's next #
Lesson 13.2 goes deeper into enter, leave and state-change animations the modern way: classes bound to signals, keyframes, staggered lists with CSS custom properties, animating to auto height, and a side-by-side table of legacy trigger/state/transition code and its CSS equivalent. Then 13.3 covers route transitions with the View Transitions API, and 13.4 makes animations reusable and accessible.
Try it yourself #
Search your project for @angular/animations. If there are no hits, add an animate.enter/animate.leave pair to a toast or notice component. If there are hits, pick the simplest component that uses transition(':enter') and rewrite it with animate.enter and a keyframe animation, then compare the bundle size after the last one is gone.
search_documentationNo. @angular/animations has been deprecated since v20.2, but it still works, so you can migrate gradually. A practical order: (1) stop adding new trigger() code and use CSS with animate.enter/animate.leave for new components; (2) migrate the components whose triggers only use :enter/:leave or two named states first, since those map directly to animate.enter, animate.leave and a class bound to a signal; (3) leave the ones built on query(), stagger() or route animations for last and replace them with CSS delays and withViewTransitions(). Migrate each component in one go, because legacy and new animations can’t be mixed in the same component. Once no component uses @angular/animations, remove provideAnimationsAsync() and the package to get the bundle-size benefit.Up next in Angular
More from this topic
Enjoyed this article?
Get new Angular tutorials delivered. No spam — just code-first articles when they ship.
1 comment



Pingback: Angular Enter, Leave and State Animations with CSS [2026]