Tutorials Logic, IN info@tutorialslogic.com

Angular Animations animate.enter, animate.leave, CSS

Animation Foundations

Modern Angular animation favors CSS with animate.enter and animate.leave for component transitions. Build motion around meaningful state changes, respect reduced-motion preferences, and use legacy animation APIs only where an existing application still requires them.

Animation Scope

Animations make an Angular application feel smoother and easier to understand. A good animation explains that something has entered the screen, left the screen, changed state, expanded, collapsed, loaded, or moved because of the user's action. Motion should guide attention and clarify interface changes rather than decorate every element.

Modern Angular recommends using CSS-based animations with animate.enter and animate.leave for new code. These APIs let Angular add animation classes at the right moment while CSS handles the actual motion. Older Angular projects may still use the @angular/animations package with trigger(), state(), and transition(), so this tutorial covers both approaches.

Motion Decisions

Animations should be short, purposeful, and predictable. Most UI animations work best between 120ms and 300ms. Slower animations are useful for large layout transitions, but they can make everyday actions feel delayed if overused.

  • Elements that appear or disappear, such as alerts, panels, modals, dropdowns, and list items.
  • State changes such as open/closed, selected/unselected, valid/invalid, and loading/ready.
  • Navigation or layout changes where users need help understanding what changed.
  • Small feedback moments such as button press states, focus rings, and successful form submission.

Enter and Leave Motion

Enter Animation

For new Angular applications, prefer animate.enter and animate.leave. They are compiler-supported animation bindings that work with CSS transitions or CSS keyframes. You do not need to import BrowserAnimationsModule or call provideAnimationsAsync() just to use these modern enter and leave animations.

The responsibility split is simple: Angular controls when the class is applied, and CSS controls how the element moves. When the animation finishes, Angular removes the temporary animation class.

This example uses modern control flow with @if. When show() becomes true, Angular adds the element to the DOM and applies the notice-enter class while the animation runs.

Basic Enter Animation

Basic Enter Animation
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-notice',
  standalone: true,
  template: `
    <button type="button" (click)="show.set(!show())">
      Toggle notice
    </button>

    @if (show()) {
      <div class="notice" animate.enter="notice-enter">
        Profile saved successfully.
      </div>
    }
  `,
  styleUrl: './notice.component.css'
})
export class NoticeComponent {
  show = signal(false);
}

Modern Angular Approach

Modern Angular Approach
.notice {
  margin-top: 12px;
  padding: 12px 14px;
  border-left: 4px solid #16a34a;
  background: #f0fdf4;
  color: #166534;
}

.notice-enter {
  animation: slide-fade-in 220ms ease-out;
}

@keyframes slide-fade-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

Enter and Leave

Most real UI elements need both an entrance and an exit. Use animate.enter for the class that runs when the element is inserted, and animate.leave for the class that runs before Angular removes the element.

The forwards value in the leave animation keeps the final hidden style while Angular waits for the animation to finish and then removes the element from the DOM.

Toast Enter and Leave

Toast Enter and Leave
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-toast-demo',
  standalone: true,
  template: `
    <button type="button" (click)="visible.set(true)">Show toast</button>
    <button type="button" (click)="visible.set(false)">Hide toast</button>

    @if (visible()) {
      <div
        class="toast"
        animate.enter="toast-enter"
        animate.leave="toast-leave"
      >
        New message received.
      </div>
    }
  `,
  styleUrl: './toast.component.css'
})
export class ToastDemoComponent {
  visible = signal(false);
}

Enter and Leave Together

Enter and Leave Together
.toast {
  width: fit-content;
  margin-top: 12px;
  padding: 10px 14px;
  border-radius: 6px;
  background: #111827;
  color: white;
  box-shadow: 0 8px 24px rgba(15, 23, 42, .18);
}

.toast-enter {
  animation: toast-in 180ms ease-out;
}

.toast-leave {
  animation: toast-out 150ms ease-in forwards;
}

@keyframes toast-in {
  from { opacity: 0; transform: translateY(10px) scale(.98); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

@keyframes toast-out {
  from { opacity: 1; transform: translateY(0) scale(1); }
  to   { opacity: 0; transform: translateY(8px) scale(.98); }
}

CSS Animation Techniques

CSS Transitions

Keyframes are good for precise multi-step motion. CSS transitions are better for simple changes between two states. With animate.enter, the animation class can define the final state and a starting style can be provided with @starting-style.

Transition with @starting-style

Transition with @starting-style
@if (open()) {
  <section class="panel" animate.enter="panel-enter">
    <h4>Account details</h4>
    <p>This panel fades and slides into place.</p>
  </section>
}

Using CSS Transitions Instead of Keyframes

Using CSS Transitions Instead of Keyframes
.panel {
  padding: 16px;
  border: 1px solid #dbeafe;
  background: #eff6ff;
}

.panel-enter {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 180ms ease-out, transform 180ms ease-out;

  @starting-style {
    opacity: 0;
    transform: translateY(10px);
  }
}

Dynamic Classes

animate.enter and animate.leave can accept a string or an expression. This is useful when a component supports different motion styles, such as sliding from the left or right depending on the action.

The matching CSS classes would define different transforms, for example translateX(-16px) for a left drawer and translateX(16px) for a right drawer.

Dynamic Animation Class

Dynamic Animation Class
import { Component, computed, signal } from '@angular/core';

@Component({
  selector: 'app-drawer',
  standalone: true,
  template: `
    <button type="button" (click)="side.set('left'); open.set(true)">Left</button>
    <button type="button" (click)="side.set('right'); open.set(true)">Right</button>
    <button type="button" (click)="open.set(false)">Close</button>

    @if (open()) {
      <aside class="drawer" [animate.enter]="enterClass()" animate.leave="drawer-leave">
        Drawer from the {{ side() }} side.
      </aside>
    }
  `
})
export class DrawerComponent {
  open = signal(false);
  side = signal<'left' | 'right'>('left');
  enterClass = computed(() => this.side() === 'left' ? 'drawer-left' : 'drawer-right');
}

Lists and State Changes

List Animation

Lists are one of the best places to use animations because adding and removing rows can otherwise feel abrupt. With modern Angular, use @for for rendering and animate.enter / animate.leave on each repeated item.

List Item Animation

List Item Animation
import { Component, signal } from '@angular/core';

type Task = { id: number; title: string };

@Component({
  selector: 'app-tasks',
  standalone: true,
  template: `
    <button type="button" (click)="addTask()">Add task</button>

    <ul class="task-list">
      @for (task of tasks(); track task.id) {
        <li class="task" animate.enter="task-enter" animate.leave="task-leave">
          <span>{{ task.title }}</span>
          <button type="button" (click)="removeTask(task.id)">Done</button>
        </li>
      } @empty {
        <li class="empty">No tasks left.</li>
      }
    </ul>
  `,
  styleUrl: './tasks.component.css'
})
export class TasksComponent {
  private nextId = 3;
  tasks = signal<Task[]>([
    { id: 1, title: 'Create animation classes' },
    { id: 2, title: 'Keep transitions short' }
  ]);

  addTask() {
    this.tasks.update(items => [
      ...items,
      { id: this.nextId, title: `Task ${this.nextId++}` }
    ]);
  }

  removeTask(id: number) {
    this.tasks.update(items => items.filter(task => task.id !== id));
  }
}

Animating Lists

Animating Lists
.task-list {
  display: grid;
  gap: 8px;
  padding: 0;
  list-style: none;
}

.task {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 12px;
  border: 1px solid #e5e7eb;
  border-radius: 6px;
}

.task-enter {
  animation: task-in 180ms ease-out;
}

.task-leave {
  animation: task-out 140ms ease-in forwards;
}

@keyframes task-in {
  from { opacity: 0; transform: translateX(-10px); }
  to   { opacity: 1; transform: translateX(0); }
}

@keyframes task-out {
  from { opacity: 1; transform: translateX(0); }
  to   { opacity: 0; transform: translateX(10px); }
}

State Animation

Not every animation needs animate.enter or animate.leave. If an element remains in the DOM and only changes state, use class binding, style binding, or CSS transitions.

This is often the cleanest approach for accordions, menus, badges, and validation states because Angular only needs to toggle a class.

Open and Closed State

Open and Closed State
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-accordion',
  standalone: true,
  template: `
    <button type="button" (click)="open.update(value => !value)">
      Toggle details
    </button>

    <div class="details" [class.details-open]="open()">
      <p>This content expands with a CSS transition.</p>
    </div>
  `,
  styleUrl: './accordion.component.css'
})
export class AccordionComponent {
  open = signal(false);
}

Animating State Changes with CSS Classes

Animating State Changes with CSS Classes
.details {
  max-height: 0;
  overflow: hidden;
  opacity: 0;
  transition: max-height 240ms ease, opacity 180ms ease;
}

.details-open {
  max-height: 160px;
  opacity: 1;
}

Accessible Performance

Reduced Motion

Animations should respect users who prefer reduced motion. CSS makes this straightforward with the prefers-reduced-motion media query. In reduced-motion mode, keep important visual state changes but remove large movement and long animation durations.

Reduced Motion CSS

Reduced Motion CSS
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}

Animation Performance

  • Prefer animating opacity and transform. They are usually cheaper than animating layout properties.
  • Avoid animating width, height, top, left, and large shadows on many elements at once.
  • Keep animations short and avoid long chains of delayed motion in everyday workflows.
  • Make sure the final UI state is clear even when animations are disabled.
  • Use stable track expressions in @for lists so Angular can animate the correct item.

Legacy Animations

Legacy Animation Setup

Older Angular tutorials and many existing projects use the @angular/animations package. In this model, animations are written in TypeScript using trigger(), state(), style(), animate(), and transition(). Angular has deprecated this package for new animation work, but understanding it is still useful when maintaining older apps.

If an existing app still uses this package, standalone applications typically enable it with provideAnimationsAsync() in application configuration. NgModule-based applications often use BrowserAnimationsModule.

Enable Legacy Animations

Enable Legacy Animations
import { ApplicationConfig } from '@angular/core';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideAnimationsAsync()
  ]
};

Legacy Trigger Example

Legacy Trigger Example
import { Component, signal } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';

@Component({
  selector: 'app-expand-card',
  standalone: true,
  animations: [
    trigger('expandCollapse', [
      state('open', style({
        height: '*',
        opacity: 1
      })),
      state('closed', style({
        height: '0',
        opacity: 0,
        overflow: 'hidden'
      })),
      transition('open <=> closed', [
        animate('220ms ease-in-out')
      ])
    ])
  ],
  template: `
    <button type="button" (click)="open.update(value => !value)">
      Toggle card
    </button>

    <section [@expandCollapse]="open() ? 'open' : 'closed'">
      <p>This uses the legacy Angular animations package.</p>
    </section>
  `
})
export class ExpandCardComponent {
  open = signal(false);
}

Legacy Animation States

The legacy package uses state expressions to decide which transition should run. These are common expressions you will see in existing Angular code:

  • open => closed runs only when the state changes from open to closed.
  • open <=> closed runs in both directions.
  • * is the wildcard state and matches any state.
  • void means the element is not currently in the DOM.
  • :enter is a shortcut for void => *.
  • :leave is a shortcut for * => void.

Choosing an Animation Model

Animation Strategy

Need Recommended approach
Element appears or disappears animate.enter and animate.leave
Simple hover, focus, selected, or expanded state CSS transitions with Angular class binding
Repeated list item insertion/removal @for with animate.enter / animate.leave on each item
Maintaining older trigger/state code Legacy @angular/animations APIs
Complex timeline or physics animation CSS keyframes or a dedicated animation library

Animation Review

In modern Angular, start with CSS and use animate.enter and animate.leave when Angular needs to coordinate DOM insertion or removal. Use normal class bindings and CSS transitions for elements that remain on the page and only change state. Learn the older @angular/animations trigger syntax so you can maintain existing code, but prefer the modern CSS-based approach for new Angular applications.

Confirm the page outcome

Motion Review

5 checks
  • Animations make an Angular application feel smoother and easier to understand.
  • A good animation explains that something has entered the screen, left the screen, changed state, expanded, collapsed, loaded, or moved because of the user's action.
  • The goal is not to decorate every element.
  • Motion should guide attention and clarify interface changes.
  • Modern Angular recommends using CSS-based animations with animate.enter and animate.leave for new code.

Motion Review Questions

Angular waits for the CSS animation or transition attached by animate.leave. If the class has no detectable duration, the animation name is misspelled, or another rule overrides animation-duration, Angular has nothing to wait for and removes the node immediately.

Usually the track expression is at fault. Tracking by $index makes identity move when rows are inserted or removed. Track task.id, and make sure IDs are stable and unique.

The browser can interpolate numeric values efficiently, but auto is a layout result rather than a fixed number.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.