Adding Animate.css to Your Project
Animate.css is a widely-used, open-source CSS library that ships with a rich set of pre-built animations. These are neatly organized into categories like entrances, exits, and attention seekers, covering a variety of interaction scenarios. Beyond the animations themselves, the library exposes utility flags for adjusting parameters such as timing, delays, and repeat counts.
Rather than rehashing the library's general documentation, let's look at how to integrate Animate.css into an Angular project, including a few practical insights I've picked up along the way.
If you'd like to jump straight to a working demo or peek at the source code, you can find it here.
Setup
You can pull in the Animate.css dependency via a CDN or through npm. The demo I prepared uses the CDN. Place the following line in your index.html file.
<head>
...
<!-- Animate.css -->
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
/>
</head>
Leveraging Angular's Class Binding
In a traditional vanilla JavaScript setup, you'd manage dynamic styles by manipulating the DOM directly with something like this:
const element = document.querySelector(".my-element");
element.classList.add("animate__animated", "animate__bounce");
Angular lets you achieve the same outcome entirely from within your template using class binding. To apply an Animate.css animation, you add the class associated with the effect (check the official list for options) plus any static configuration classes. That class can then be bound to a component property to control the trigger. As an example, imagine you want a bounce effect when a boolean property called shouldBounce flips to true. You'd bind the animate__bounce class directly to that property.
<div class="animate__animated" [class.animate__bounce]="shouldBounce"></div>
To fine-tune your animations—such as setting durations, delays, or iteration counts—you have two options. If these settings are fixed, add the relevant classes to the element's static
classattribute. If they need to be dynamic, follow the binding approach shown above, tying them to component state.
Bringing JavaScript Into the Equation
The class binding method is solid for many situations, but it falls short when you need to replay the same animation repeatedly. A common example is a button that plays an animation on each click, not just the first one.
This limitation exists because Animate.css triggers an animation by adding its class; if that class is already present, the animation won't restart. To get around this, you can tap into the animationend event. By listening for when the animation completes, you can remove the class that initiated it. This resets the element's state, allowing you to add the class back and trigger the animation again whenever you need.
import { ViewChild, ElementRef } from '@angular/core';
...
export class AnimateStyleComponent {
@ViewChild('cardContainer') cardContainer: ElementRef;
bounceCard(): void {
this._animate('bounce').catch((e) => {
console.error('Error animating element:', e);
});
}
private _animate(animationName: string, persistClass = false): Promise<void> {
if (!this.cardContainer || !this.cardContainer.nativeElement) {
return Promise.reject('element not defined');
}
if (this.isAnimating) {
return Promise.reject('element is animating');
}
return new Promise((resolve) => {
this.isAnimating = true;
// listen to animationend to allow additional logic to be run
// after the animation from Animate.css is done executing
this.cardContainer.nativeElement.addEventListener(
'animationend',
(event) => {
if (event.animationName === animationName) {
this.isAnimating = false;
if (!persistClass) {
this.cardContainer.nativeElement.classList = '';
}
resolve();
}
},
{ once: true }
);
this.cardContainer.nativeElement.classList = `animate__animated animate__${animationName}`;
});
}
}
Transitions In and Out
Entrance and exit animations are just as easy—all we need is a conditional class binding on the target element.
<div
class="animate__animated"
[class.animate__zoomInDown]="isShowing"
[class.fadeOut]="!isShowing"
></div>
That said, this technique only moves the element in and out visually. It does not actually take the element out of the DOM the way an *ngIf would.
Here’s what happens when we pair an *ngIf with a class binding entirely inside the template:
<div
*ngIf="isShowing"
class="animate__animated"
[class.animate__zoomInDown]="isShowing"
[class.fadeOut]="!isShowing"
></div>
Which produces this result:
As you can see, the entry animation runs, but the exit animation is never triggered. That’s because *ngIf destroys the element immediately once the condition turns false—it doesn’t pause to let an animation finish.
You could drop the
*ngIfentirely and the animation would run correctly. But for certain animation types, the element would still occupy space in the DOM and could block content behind it. It would only be animated out, not truly removed.
To fix this properly, we need a slightly different strategy that involves a bit of JavaScript.
Adding and removing elements from the DOM requires a bit of extra setup. First, we wrap the component we want to animate inside an ng-template so we control when it gets inserted or removed. We also set opacity to 0 so the view doesn’t flash on screen before the enter animation starts. We’ll revisit this soon.
<div #container></div>
<ng-template #template>
<!-- set opacity to 0 to prevent flashing before enter animation starts -->
<div #cardContainer [style.opacity]="0">
<app-card-demo-sample></app-card-demo-sample>
</div>
</ng-template>
</div>
Next, we grab a reference to that template and its parent container so we can insert and remove it programmatically.
export class AnimateStyleDemoComponent {
@ViewChild("container", { read: ViewContainerRef })
container: ViewContainerRef;
@ViewChild("cardContainer") cardContainer: ElementRef;
@ViewChild("template", { read: TemplateRef }) template: TemplateRef<null>;
private _addCardToView(): Promise<void> {
return new Promise((resolve) => {
if (!this.viewRef) {
this.container.clear();
// add element to container
this.viewRef = this.container.createEmbeddedView(this.template);
// wrap this in a settimeout if it tries to animate before view is loaded
if (this.cardContainer && this.cardContainer.nativeElement) {
// set opacity to 1 to make element visible before starting enter animation
this.renderer.setStyle(
this.cardContainer.nativeElement,
"opacity",
1
);
}
resolve();
});
} else {
resolve();
}
});
}
private _removeCardFromView(): void {
this.container.clear();
if (this.viewRef) {
this.viewRef.destroy();
this.viewRef = null;
}
}
}
To coordinate the DOM insertion/removal with the animation, we chain the two steps using promises. For incoming elements, we first attach the card to the view and then run the animation. For outgoing elements, we reverse the order—animate first, then detach the card. To confirm this sequence, we can add console.log statements in the component’s ngOnInit and ngOnDestroy, which should fire when the component is created and destroyed accordingly.
export class AnimateStyleDemoComponent {
private _showCard(): void {
this._addCardToView().then(() => {
this._animate("zoomInDown").catch((e) => {
console.error("Error animating element:", e);
});
});
}
private _hideCard(): void {
this._animate("fadeOut", true)
.then(() => {
this._removeCardFromView();
})
.catch((e) => {
console.error("Error animating element:", e);
});
}
}
Final Thoughts
That wraps things up for now. I’ll keep adding posts about different ways to bring animation into Angular—covering Angular’s built-in animation system, a few handy libraries, and some useful tricks. If this type of content is useful or if you have questions, drop a comment or tweet me at @williamjuan27.

