Setting Up a Basic List
Angular ships with a full-featured animation module, which comes in handy for handling elements as they appear on screen or as they are removed from it. Beyond that, the AnimationBuilder service lets you take control of custom animations in an imperative way, so you can start, pause, or halt them whenever needed. Let's walk through the mechanics.
We begin by constructing a list, much like the one you might have seen in related demos:
@Component({
selector: 'app-root',
standalone: true,
template: `
<button (click)="addUser()">Add user</button>
<ul>
@for (user of users(); track user.id) {
<li>{{ user.name }}</li>
}
</ul>
`,
})
export class AppComponent {
users = signal<User[]>([
{ id: Math.random(), name: 'Michele' }
]);
addUser() {
this.users.update(users => [...users, { id: Math.random(), name: 'New user' }]);
}
}
As you might observe, there's a button positioned within the template whose function is to append a new user entry to the list.
Bringing the List to Life with Animations
Suppose we want a fresh animation to run whenever a user gets added. The first thing to do is to register the animation capabilities with Angular's core configuration setup:
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
bootstrapApplication(AppComponent, {
providers: [
provideAnimationsAsync(),
]
});
After that, we can define the animation logic itself:
import { trigger, transition, style, animate } from '@angular/animations';
const fadeInAnimation = trigger('fadeIn', [
transition(':enter', [
style({ transform: 'scale(0.5)', opacity: 0 }),
animate(
'.3s cubic-bezier(.8, -0.6, 0.2, 1.5)',
style({ transform: 'scale(1)', opacity: 1 })
)
])
])
Through these helper steps, we:
- Set up an animation trigger labeled
fadeIn - Established a transition that fires at the moment the element first appears in the DOM
- Assigned an initial set of styles for the entry state
- Kicked off the animation right away, which alters the element's styles progressively
If you need a deeper dive into crafting animations, do check out the official documentation; it's a solid starting point.
Next, we attach this trigger to every item inside our list template:
@Component({
...,
template: `
<button (click)="addUser()">Add user</button>
<ul>
@for (user of users(); track user.id) {
<li @fadeIn>{{ user.name }}</li> <!-- Notice here -->
}
</ul>
`,
// Also, add the animation to the metadata of the component
animations: [fadeInAnimation]
})
With this in place, you'll get an animation each time an entry gets added. That covers our initial objective.
One important nuance: for the animation to behave properly, Angular must be able to distinguish between individual elements in the loop. If it can't, it might end up reusing or recreating the same DOM nodes during template updates, which could trigger the animation unintentionally more than once. With the modern Control Flow approach, this is conveniently handled since the track property is a requirement. However, if you're on an earlier Angular release using *ngFor, you'll need to rely on the trackBy option to keep the tracking explicit, like so:
<li
*ngFor="let user of users; trackBy: trackByUserId"
@fadeIn
>{{ user.name }}</li>
// A class method in your component:
trackByUserId(index, user: User) {
return user.id;
}
Alright, with that settled, let's move forward and add a second form of animation to the list.
AnimationBuilder
Now, let's attach a button to every item in the list:
<li @fadeIn>
{{ user.name }}
<button>Make me blink</button>
</li>
Consider this scenario: when the button gets clicked, we want the corresponding element to flash. That would be a nice touch! The AnimationBuilder service is exactly what we need for this.
To start, we'll build a Directive and attach it to each element. Inside this directive, we'll inject both ElementRef and AnimationBuilder:
import { AnimationBuilder, style, animate } from '@angular/animations';
@Directive({
selector: '[blink]',
exportAs: 'blink', // <--- Notice
standalone: true
})
export class BlinkDirective {
private animationBuilder = inject(AnimationBuilder);
private el = inject(ElementRef);
}
You'll notice we exported the directive — we'll explain why shortly.
Next, we can define a custom animation in this manner:
export class BlinkDirective {
...
private animation = this.animationBuilder.build([
style({ transform: 'scale(1)', opacity: 1 }),
animate(150, style({ transform: 'scale(1.1)', opacity: .5 })),
animate(150, style({ transform: 'scale(1)', opacity: 1 }))
]);
}
The functions here are the same ones we relied on for the previous animation; only the styles differ.
Our next move is to instantiate a player that will run the animation on our target element:
export class BlinkDirective {
...
private player = this.animation.create(this.el.nativeElement);
}
Now, let's introduce a public method that kicks off the animation when called:
export class BlinkDirective {
...
start() {
this.player.play();
}
}
One final piece remains: we need to import the directive, apply it to the elements, capture it via a template variable, and invoke the method on button click!
@Component({
selector: 'app-root',
standalone: true,
template: `
<button (click)="addUser()">Add user</button>
<ul>
@for (user of users(); track user.id) {
<li @fadeIn blink #blinkDir="blink">
{{ user.name }}
<button (click)="blinkDir.start()">Make me blink</button>
</li>
}
</ul>
`,
imports: [BlinkDirective],
animations: [
fadeInAnimation
]
})
Because we set exportAs on the directive earlier, we're able to reference its instance through a local variable. That's the crucial detail!
Go ahead and click the button — the element should animate as expected.
This wraps up the exercise, but it only scratches the surface! The AnimationPlayer exposes a rich set of methods, letting you halt, pause, or resume animations at will. Pretty powerful stuff!
interface AnimationPlayer {
onDone(fn: () => void): void;
onStart(fn: () => void): void;
onDestroy(fn: () => void): void;
init(): void;
hasStarted(): boolean;
play(): void;
pause(): void;
restart(): void;
finish(): void;
destroy(): void;
reset(): void;
setPosition(position: number): void;
getPosition(): number;
parentPlayer: AnimationPlayer;
readonly totalTime: number;
beforeDestroy?: () => any;
}
For your convenience, here's the complete example: drop it into your main.ts file and watch it work!
import { Component, signal, Directive, ElementRef, inject } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { trigger, transition, style, animate, AnimationBuilder } from '@angular/animations';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
interface User {
id: number;
name: string;
}
@Directive({
selector: '[blink]',
exportAs: 'blink',
standalone: true
})
export class BlinkDirective {
private animationBuilder = inject(AnimationBuilder);
private el = inject(ElementRef);
private animation = this.animationBuilder.build([
style({ transform: 'scale(1)', opacity: 1 }),
animate(150, style({ transform: 'scale(1.1)', opacity: .5 })),
animate(150, style({ transform: 'scale(1)', opacity: 1 }))
]);
private player = this.animation.create(this.el.nativeElement);
start() {
this.player.play();
}
}
const fadeInAnimation = trigger('fadeIn', [
transition(':enter', [
style({ transform: 'scale(0.5)', opacity: 0 }),
animate(
'.3s cubic-bezier(.8, -0.6, 0.2, 1.5)',
style({ transform: 'scale(1)', opacity: 1 })
)
])
])
@Component({
selector: 'app-root',
standalone: true,
template: `
<button (click)="addUser()">Add user</button>
<ul>
@for (user of users(); track user.id) {
<li @fadeIn blink #blinkDir="blink">
{{ user.name }}
<button (click)="blinkDir.start()">Make me blink</button>
</li>
}
</ul>
`,
imports: [BlinkDirective],
animations: [
fadeInAnimation
]
})
export class App {
users = signal<User[]>([
{ id: Math.random(), name: 'Michele' }
]);
addUser() {
this.users.update(users => [...users, { id: Math.random(), name: 'New user' }]);
}
}
bootstrapApplication(App, {
providers: [
provideAnimationsAsync()
]
});
AccademiaDev: text-based web development courses!
My philosophy centers on delivering focused, high-value material that skips the padding and fluff typical of conventional textbooks. Leveraging my experience as both a consultant and trainer, these offerings—structured as interactive online courses—pack practical knowledge into text, code snippets, and quizzes, making for a streamlined and engaging way to learn.


