Angular Animations — animate.enter, animate.leave, CSS
Angular Animations
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. The goal is to guide attention and make interface changes feel intentional.
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.
What You Should Animate
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.
Modern Angular Approach
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 mental model 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.
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);
}
.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);
}
}
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.
Enter and Leave Together
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.
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);
}
.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); }
}
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.
Using CSS Transitions Instead of Keyframes
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.
@if (open()) {
<section class="panel" animate.enter="panel-enter">
<h4>Account details</h4>
<p>This panel fades and slides into place.</p>
</section>
}
.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 Animation 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.
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');
}
The matching CSS classes would define different transforms, for example translateX(-16px) for a left drawer and translateX(16px) for a right drawer.
Animating Lists
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.
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));
}
}
.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); }
}
Animating State Changes with CSS Classes
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.
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);
}
.details {
max-height: 0;
overflow: hidden;
opacity: 0;
transition: max-height 240ms ease, opacity 180ms ease;
}
.details-open {
max-height: 160px;
opacity: 1;
}
This is often the cleanest approach for accordions, menus, badges, and validation states because Angular only needs to toggle a class.
Accessibility and 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.
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 1ms !important;
animation-iteration-count: 1 !important;
transition-duration: 1ms !important;
scroll-behavior: auto !important;
}
}
Performance Best Practices
Legacy Angular Animations Package
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.
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()
]
};
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:
When to Use Which Approach
| 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 |
Summary
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.
-
Modern Angular animations usually use
animate.enterandanimate.leavewith CSS. - Use CSS transitions for state changes when the element stays in the DOM.
- Use CSS keyframes when an animation needs a clear multi-step motion path.
-
Prefer animating
opacityandtransformfor smoother performance. -
Respect
prefers-reduced-motionso users can reduce large or distracting movement. -
The older
@angular/animationspackage is still common in existing projects, but it is deprecated for new animation work.
Using the legacy @angular/animations package for every new animation
Use animate.enter, animate.leave, and CSS for new enter/leave effects
Animating height, top, left, or width across many elements
Animate transform and opacity whenever possible
Forgetting reduced-motion users
Add a prefers-reduced-motion rule
Frequently Asked Questions
Level Up Your Angular Skills
Master Angular with these hand-picked resources