Angular Animations in 2026: animate.enter, animate.leave and CSS [2026]

Link copied
Angular Animations in 2026: animate.enter, animate.leave and CSS [2026]

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 a transition, you need to give the browser a "before" style; the CSS @starting-style rule 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.

YouWe just upgraded to Angular 22 and still have provideAnimationsAsync() plus about 30 components with trigger() animations. Do we have to rewrite them all now?
Claude · used 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

View all Angular articles →
Angular

When Angular is launched ?

Link copied Angular When Angular is launched ? February 8, 2024 · 1 min read When was Angular launched? # Angu…

Feb 8, 2024 Read →

Enjoyed this article?

Get new Angular tutorials delivered. No spam — just code-first articles when they ship.

1 comment

1 thought on “Angular Animations in 2026: animate.enter, animate.leave and CSS [2026]”

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

Leave a Comment

Your email stays private. Required fields are marked *

1 thought on “Angular Animations in 2026: animate.enter, animate.leave and CSS [2026]”

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

Leave a Comment

Your email stays private. Required fields are marked *