This guide goes into depth on special transition states such as the *
wildcard and void
. It shows how these special states are used for elements entering and leaving a view.
This section also explores multiple animation triggers, animation callbacks, and sequence-based animation using keyframes.
Predefined states and wildcard matching
In Angular, transition states can be defined explicitly through the state()
function, or using the predefined *
wildcard and void
states.
Wildcard state
An asterisk *
or wildcard matches any animation state.
This is useful for defining transitions that apply regardless of the HTML element's start or end state.
For example, a transition of open => *
applies when the element's state changes from open to anything else.
The following is another code sample using the wildcard state together with the previous example using the open
and closed
states.
Instead of defining each state-to-state transition pair, any transition to closed
takes 1 second, and any transition to open
takes 0.5 seconds.
This allows the addition of new states without having to include separate transitions for each one.
src/app/open-close.component.ts
import {Component, Input} from '@angular/core';import {trigger, transition, state, animate, style, AnimationEvent} from '@angular/animations';@Component({ selector: 'app-open-close', animations: [ trigger('openClose', [ // ... state( 'open', style({ height: '200px', opacity: 1, backgroundColor: 'yellow', }), ), state( 'closed', style({ height: '100px', opacity: 0.8, backgroundColor: 'blue', }), ), transition('open => closed', [animate('1s')]), transition('closed => open', [animate('0.5s')]), transition('* => closed', [animate('1s')]), transition('* => open', [animate('0.5s')]), transition('open <=> closed', [animate('0.5s')]), transition('* => open', [animate('1s', style({opacity: '*'}))]), transition('* => *', [animate('1s')]), ]), ], templateUrl: 'open-close.component.html', styleUrls: ['open-close.component.css'],})export class OpenCloseComponent { @Input() logging = false; isOpen = true; toggle() { this.isOpen = !this.isOpen; } onAnimationEvent(event: AnimationEvent) { if (!this.logging) { return; } // openClose is trigger name in this example console.warn(`Animation Trigger: ${event.triggerName}`); // phaseName is "start" or "done" console.warn(`Phase: ${event.phaseName}`); // in our example, totalTime is 1000 (number of milliseconds in a second) console.warn(`Total time: ${event.totalTime}`); // in our example, fromState is either "open" or "closed" console.warn(`From: ${event.fromState}`); // in our example, toState either "open" or "closed" console.warn(`To: ${event.toState}`); // the HTML element itself, the button in this case console.warn(`Element: ${event.element}`); }}
Use a double arrow syntax to specify state-to-state transitions in both directions.
src/app/open-close.component.ts
import {Component, Input} from '@angular/core';import {trigger, transition, state, animate, style, AnimationEvent} from '@angular/animations';@Component({ selector: 'app-open-close', animations: [ trigger('openClose', [ // ... state( 'open', style({ height: '200px', opacity: 1, backgroundColor: 'yellow', }), ), state( 'closed', style({ height: '100px', opacity: 0.8, backgroundColor: 'blue', }), ), transition('open => closed', [animate('1s')]), transition('closed => open', [animate('0.5s')]), transition('* => closed', [animate('1s')]), transition('* => open', [animate('0.5s')]), transition('open <=> closed', [animate('0.5s')]), transition('* => open', [animate('1s', style({opacity: '*'}))]), transition('* => *', [animate('1s')]), ]), ], templateUrl: 'open-close.component.html', styleUrls: ['open-close.component.css'],})export class OpenCloseComponent { @Input() logging = false; isOpen = true; toggle() { this.isOpen = !this.isOpen; } onAnimationEvent(event: AnimationEvent) { if (!this.logging) { return; } // openClose is trigger name in this example console.warn(`Animation Trigger: ${event.triggerName}`); // phaseName is "start" or "done" console.warn(`Phase: ${event.phaseName}`); // in our example, totalTime is 1000 (number of milliseconds in a second) console.warn(`Total time: ${event.totalTime}`); // in our example, fromState is either "open" or "closed" console.warn(`From: ${event.fromState}`); // in our example, toState either "open" or "closed" console.warn(`To: ${event.toState}`); // the HTML element itself, the button in this case console.warn(`Element: ${event.element}`); }}
Use wildcard state with multiple transition states
In the two-state button example, the wildcard isn't that useful because there are only two possible states, open
and closed
.
In general, use wildcard states when an element has multiple potential states that it can change to.
If the button can change from open
to either closed
or something like inProgress
, using a wildcard state could reduce the amount of coding needed.
src/app/open-close.component.ts
import {Component, Input} from '@angular/core';import {trigger, transition, state, animate, style, AnimationEvent} from '@angular/animations';@Component({ selector: 'app-open-close', animations: [ trigger('openClose', [ // ... state( 'open', style({ height: '200px', opacity: 1, backgroundColor: 'yellow', }), ), state( 'closed', style({ height: '100px', opacity: 0.8, backgroundColor: 'blue', }), ), transition('open => closed', [animate('1s')]), transition('closed => open', [animate('0.5s')]), transition('* => closed', [animate('1s')]), transition('* => open', [animate('0.5s')]), transition('open <=> closed', [animate('0.5s')]), transition('* => open', [animate('1s', style({opacity: '*'}))]), transition('* => *', [animate('1s')]), ]), ], templateUrl: 'open-close.component.html', styleUrls: ['open-close.component.css'],})export class OpenCloseComponent { @Input() logging = false; isOpen = true; toggle() { this.isOpen = !this.isOpen; } onAnimationEvent(event: AnimationEvent) { if (!this.logging) { return; } // openClose is trigger name in this example console.warn(`Animation Trigger: ${event.triggerName}`); // phaseName is "start" or "done" console.warn(`Phase: ${event.phaseName}`); // in our example, totalTime is 1000 (number of milliseconds in a second) console.warn(`Total time: ${event.totalTime}`); // in our example, fromState is either "open" or "closed" console.warn(`From: ${event.fromState}`); // in our example, toState either "open" or "closed" console.warn(`To: ${event.toState}`); // the HTML element itself, the button in this case console.warn(`Element: ${event.element}`); }}
The * => *
transition applies when any change between two states takes place.
Transitions are matched in the order in which they are defined.
Thus, you can apply other transitions on top of the * => *
transition.
For example, define style changes or animations that would apply just to open => closed
, then use * => *
as a fallback for state pairings that aren't otherwise called out.
To do this, list the more specific transitions before * => *
.
Use wildcards with styles
Use the wildcard *
with a style to tell the animation to use whatever the current style value is, and animate with that.
Wildcard is a fallback value that's used if the state being animated isn't declared within the trigger.
src/app/open-close.component.ts
import {Component, Input} from '@angular/core';import {trigger, transition, state, animate, style, AnimationEvent} from '@angular/animations';@Component({ selector: 'app-open-close', animations: [ trigger('openClose', [ // ... state( 'open', style({ height: '200px', opacity: 1, backgroundColor: 'yellow', }), ), state( 'closed', style({ height: '100px', opacity: 0.8, backgroundColor: 'blue', }), ), transition('open => closed', [animate('1s')]), transition('closed => open', [animate('0.5s')]), transition('* => closed', [animate('1s')]), transition('* => open', [animate('0.5s')]), transition('open <=> closed', [animate('0.5s')]), transition('* => open', [animate('1s', style({opacity: '*'}))]), transition('* => *', [animate('1s')]), ]), ], templateUrl: 'open-close.component.html', styleUrls: ['open-close.component.css'],})export class OpenCloseComponent { @Input() logging = false; isOpen = true; toggle() { this.isOpen = !this.isOpen; } onAnimationEvent(event: AnimationEvent) { if (!this.logging) { return; } // openClose is trigger name in this example console.warn(`Animation Trigger: ${event.triggerName}`); // phaseName is "start" or "done" console.warn(`Phase: ${event.phaseName}`); // in our example, totalTime is 1000 (number of milliseconds in a second) console.warn(`Total time: ${event.totalTime}`); // in our example, fromState is either "open" or "closed" console.warn(`From: ${event.fromState}`); // in our example, toState either "open" or "closed" console.warn(`To: ${event.toState}`); // the HTML element itself, the button in this case console.warn(`Element: ${event.element}`); }}
Void state
Use the void
state to configure transitions for an element that is entering or leaving a page.
See Animating entering and leaving a view.
Combine wildcard and void states
Combine wildcard and void states in a transition to trigger animations that enter and leave the page:
- A transition of
* => void
applies when the element leaves a view, regardless of what state it was in before it left - A transition of
void => *
applies when the element enters a view, regardless of what state it assumes when entering - The wildcard state
*
matches to any state, includingvoid
Animate entering and leaving a view
This section shows how to animate elements entering or leaving a page.
Add a new behavior:
- When you add a hero to the list of heroes, it appears to fly onto the page from the left
- When you remove a hero from the list, it appears to fly out to the right
src/app/hero-list-enter-leave.component.ts
import {Component, Input, Output, EventEmitter} from '@angular/core';import {trigger, state, style, animate, transition} from '@angular/animations';import {Hero} from './hero';import {NgFor} from '@angular/common';@Component({ selector: 'app-hero-list-enter-leave', template: ` <ul class="heroes"> @for (hero of heroes; track hero) { <li [@flyInOut]="'in'"> <button class="inner" type="button" (click)="removeHero(hero.id)"> <span class="badge">{{ hero.id }}</span> <span class="name">{{ hero.name }}</span> </button> </li> } </ul> `, styleUrls: ['./hero-list-page.component.css'], imports: [NgFor], animations: [ trigger('flyInOut', [ state('in', style({transform: 'translateX(0)'})), transition('void => *', [style({transform: 'translateX(-100%)'}), animate(100)]), transition('* => void', [animate(100, style({transform: 'translateX(100%)'}))]), ]), ],})export class HeroListEnterLeaveComponent { @Input() heroes: Hero[] = []; @Output() remove = new EventEmitter<number>(); removeHero(id: number) { this.remove.emit(id); }}
In the preceding code, you applied the void
state when the HTML element isn't attached to a view.
Aliases :enter and :leave
:enter
and :leave
are aliases for the void => *
and * => void
transitions.
These aliases are used by several animation functions.
transition ( ':enter', [ … ] ); // alias for void => *transition ( ':leave', [ … ] ); // alias for * => void
It's harder to target an element that is entering a view because it isn't in the DOM yet.
Use the aliases :enter
and :leave
to target HTML elements that are inserted or removed from a view.
Use *ngIf
and *ngFor
with :enter and :leave
The :enter
transition runs when any *ngIf
or *ngFor
views are placed on the page, and :leave
runs when those views are removed from the page.
IMPORTANT: Entering/leaving behaviors can sometime be confusing.
As a rule of thumb consider that any element being added to the DOM by Angular passes via the :enter
transition. Only elements being directly removed from the DOM by Angular pass via the :leave
transition. For example, an element's view is removed from the DOM because its parent is being removed from the DOM.
This example has a special trigger for the enter and leave animation called myInsertRemoveTrigger
.
The HTML template contains the following code.
src/app/insert-remove.component.html
<h2>Insert/Remove</h2><nav> <button type="button" (click)="toggle()">Toggle Insert/Remove</button></nav>@if (isShown) { <div @myInsertRemoveTrigger class="insert-remove-container"> <p>The box is inserted</p> </div>}
In the component file, the :enter
transition sets an initial opacity of 0. It then animates it to change that opacity to 1 as the element is inserted into the view.
src/app/insert-remove.component.ts
import {Component} from '@angular/core';import {trigger, transition, animate, style} from '@angular/animations';import {NgIf} from '@angular/common';@Component({ selector: 'app-insert-remove', imports: [NgIf], animations: [ trigger('myInsertRemoveTrigger', [ transition(':enter', [style({opacity: 0}), animate('100ms', style({opacity: 1}))]), transition(':leave', [animate('100ms', style({opacity: 0}))]), ]), ], templateUrl: 'insert-remove.component.html', styleUrls: ['insert-remove.component.css'],})export class InsertRemoveComponent { isShown = false; toggle() { this.isShown = !this.isShown; }}
Note that this example doesn't need to use state()
.
Transition :increment and :decrement
The transition()
function takes other selector values, :increment
and :decrement
.
Use these to kick off a transition when a numeric value has increased or decreased in value.
HELPFUL: The following example uses query()
and stagger()
methods.
For more information on these methods, see the complex sequences page.
src/app/hero-list-page.component.ts
import {Component, HostBinding, OnInit} from '@angular/core';import {trigger, transition, animate, style, query, stagger} from '@angular/animations';import {HEROES} from './mock-heroes';import {Hero} from './hero';import {NgFor} from '@angular/common';@Component({ imports: [NgFor], selector: 'app-hero-list-page', templateUrl: 'hero-list-page.component.html', styleUrls: ['hero-list-page.component.css'], animations: [ trigger('pageAnimations', [ transition(':enter', [ query('.hero', [ style({opacity: 0, transform: 'translateY(-100px)'}), stagger(30, [ animate('500ms cubic-bezier(0.35, 0, 0.25, 1)', style({opacity: 1, transform: 'none'})), ]), ]), ]), ]), trigger('filterAnimation', [ transition(':enter, * => 0, * => -1', []), transition(':increment', [ query( ':enter', [ style({opacity: 0, width: 0}), stagger(50, [animate('300ms ease-out', style({opacity: 1, width: '*'}))]), ], {optional: true}, ), ]), transition(':decrement', [ query(':leave', [stagger(50, [animate('300ms ease-out', style({opacity: 0, width: 0}))])]), ]), ]), ],})export class HeroListPageComponent implements OnInit { @HostBinding('@pageAnimations') public animatePage = true; heroesTotal = -1; get heroes() { return this._heroes; } private _heroes: Hero[] = []; ngOnInit() { this._heroes = HEROES; } updateCriteria(criteria: string) { criteria = criteria ? criteria.trim() : ''; this._heroes = HEROES.filter((hero) => hero.name.toLowerCase().includes(criteria.toLowerCase()), ); const newTotal = this.heroes.length; if (this.heroesTotal !== newTotal) { this.heroesTotal = newTotal; } else if (!criteria) { this.heroesTotal = -1; } }}
Boolean values in transitions
If a trigger contains a Boolean value as a binding value, then this value can be matched using a transition()
expression that compares true
and false
, or 1
and 0
.
src/app/open-close.component.html
<nav> <button type="button" (click)="toggle()">Toggle Boolean/Close</button></nav><div [@openClose]="isOpen ? true : false" class="open-close-container"> <p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p></div>
In the code snippet above, the HTML template binds a <div>
element to a trigger named openClose
with a status expression of isOpen
, and with possible values of true
and false
.
This pattern is an alternative to the practice of creating two named states like open
and close
.
Inside the @Component
metadata under the animations:
property, when the state evaluates to true
, the associated HTML element's height is a wildcard style or default.
In this case, the animation uses whatever height the element already had before the animation started.
When the element is closed
, the element gets animated to a height of 0, which makes it invisible.
src/app/open-close.component.ts
import {Component} from '@angular/core';import {trigger, transition, state, animate, style} from '@angular/animations';@Component({ selector: 'app-open-close-boolean', animations: [ trigger('openClose', [ state('true', style({height: '*'})), state('false', style({height: '0px'})), transition('false <=> true', animate(500)), ]), ], templateUrl: 'open-close.component.2.html', styleUrls: ['open-close.component.css'],})export class OpenCloseBooleanComponent { isOpen = false; toggle() { this.isOpen = !this.isOpen; }}
Multiple animation triggers
You can define more than one animation trigger for a component. Attach animation triggers to different elements, and the parent-child relationships among the elements affect how and when the animations run.
Parent-child animations
Each time an animation is triggered in Angular, the parent animation always gets priority and child animations are blocked.
For a child animation to run, the parent animation must query each of the elements containing child animations. It then lets the animations run using the animateChild()
function.
Disable an animation on an HTML element
A special animation control binding called @.disabled
can be placed on an HTML element to turn off animations on that element, as well as any nested elements.
When true, the @.disabled
binding prevents all animations from rendering.
The following code sample shows how to use this feature.
src/app/open-close.component.html
<nav> <button type="button" (click)="toggleAnimations()">Toggle Animations</button> <button type="button" (click)="toggle()">Toggle Open/Closed</button></nav><div [@.disabled]="isDisabled"> <div [@childAnimation]="isOpen ? 'open' : 'closed'" class="open-close-container"> <p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p> </div></div>
src/app/open-close.component.ts
import {Component} from '@angular/core';import {trigger, transition, state, animate, style} from '@angular/animations';@Component({ selector: 'app-open-close-toggle', templateUrl: 'open-close.component.4.html', styleUrls: ['open-close.component.css'], animations: [ trigger('childAnimation', [ // ... state( 'open', style({ width: '250px', opacity: 1, backgroundColor: 'yellow', }), ), state( 'closed', style({ width: '100px', opacity: 0.8, backgroundColor: 'blue', }), ), transition('* => *', [animate('1s')]), ]), ],})export class OpenCloseChildComponent { isDisabled = false; isOpen = false; toggleAnimations() { this.isDisabled = !this.isDisabled; } toggle() { this.isOpen = !this.isOpen; }}
When the @.disabled
binding is true, the @childAnimation
trigger doesn't kick off.
When an element within an HTML template has animations turned off using the @.disabled
host binding, animations are turned off on all inner elements as well.
You can't selectively turn off multiple animations on a single element.
A selective child animations can still be run on a disabled parent in one of the following ways:
- A parent animation can use the
query()
function to collect inner elements located in disabled areas of the HTML template. Those elements can still animate.
- A child animation can be queried by a parent and then later animated with the
animateChild()
function
Disable all animations
To turn off all animations for an Angular application, place the @.disabled
host binding on the topmost Angular component.
src/app/app.component.ts
import {Component, HostBinding} from '@angular/core';import { trigger, state, style, animate, transition, // ...} from '@angular/animations';import {ChildrenOutletContexts, RouterLink, RouterOutlet} from '@angular/router';import {slideInAnimation} from './animations';@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrls: ['app.component.css'], imports: [RouterLink, RouterOutlet], animations: [ slideInAnimation, // animation triggers go here ],})export class AppComponent { @HostBinding('@.disabled') public animationsDisabled = false; constructor(private contexts: ChildrenOutletContexts) {} getRouteAnimationData() { return this.contexts.getContext('primary')?.route?.snapshot?.data?.['animation']; } toggleAnimations() { this.animationsDisabled = !this.animationsDisabled; }}
HELPFUL: Disabling animations application-wide is useful during end-to-end (E2E) testing.
Animation callbacks
The animation trigger()
function emits callbacks when it starts and when it finishes.
The following example features a component that contains an openClose
trigger.
src/app/open-close.component.ts
import {Component, Input} from '@angular/core';import {trigger, transition, state, animate, style, AnimationEvent} from '@angular/animations';@Component({ selector: 'app-open-close', animations: [ trigger('openClose', [ // ... state( 'open', style({ height: '200px', opacity: 1, backgroundColor: 'yellow', }), ), state( 'closed', style({ height: '100px', opacity: 0.8, backgroundColor: 'blue', }), ), transition('open => closed', [animate('1s')]), transition('closed => open', [animate('0.5s')]), transition('* => closed', [animate('1s')]), transition('* => open', [animate('0.5s')]), transition('open <=> closed', [animate('0.5s')]), transition('* => open', [animate('1s', style({opacity: '*'}))]), transition('* => *', [animate('1s')]), ]), ], templateUrl: 'open-close.component.html', styleUrls: ['open-close.component.css'],})export class OpenCloseComponent { @Input() logging = false; isOpen = true; toggle() { this.isOpen = !this.isOpen; } onAnimationEvent(event: AnimationEvent) { if (!this.logging) { return; } // openClose is trigger name in this example console.warn(`Animation Trigger: ${event.triggerName}`); // phaseName is "start" or "done" console.warn(`Phase: ${event.phaseName}`); // in our example, totalTime is 1000 (number of milliseconds in a second) console.warn(`Total time: ${event.totalTime}`); // in our example, fromState is either "open" or "closed" console.warn(`From: ${event.fromState}`); // in our example, toState either "open" or "closed" console.warn(`To: ${event.toState}`); // the HTML element itself, the button in this case console.warn(`Element: ${event.element}`); }}
In the HTML template, the animation event is passed back via $event
, as @triggerName.start
and @triggerName.done
, where triggerName
is the name of the trigger being used.
In this example, the trigger openClose
appears as follows.
src/app/open-close.component.html
<nav> <button type="button" (click)="toggle()">Toggle Open/Close</button></nav> <div [@openClose]="isOpen ? 'open' : 'closed'" (@openClose.start)="onAnimationEvent($event)" (@openClose.done)="onAnimationEvent($event)" class="open-close-container"> <p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p></div>
A potential use for animation callbacks could be to cover for a slow API call, such as a database lookup. For example, an InProgress button can be set up to have its own looping animation while the backend system operation finishes.
Another animation can be called when the current animation finishes.
For example, the button goes from the inProgress
state to the closed
state when the API call is completed.
An animation can influence an end user to perceive the operation as faster, even when it is not.
Callbacks can serve as a debugging tool, for example in conjunction with console.warn()
to view the application's progress in a browser's Developer JavaScript Console.
The following code snippet creates console log output for the original example, a button with the two states of open
and closed
.
src/app/open-close.component.ts
import {Component, Input} from '@angular/core';import {trigger, transition, state, animate, style, AnimationEvent} from '@angular/animations';@Component({ selector: 'app-open-close', animations: [ trigger('openClose', [ // ... state( 'open', style({ height: '200px', opacity: 1, backgroundColor: 'yellow', }), ), state( 'closed', style({ height: '100px', opacity: 0.8, backgroundColor: 'blue', }), ), transition('open => closed', [animate('1s')]), transition('closed => open', [animate('0.5s')]), transition('* => closed', [animate('1s')]), transition('* => open', [animate('0.5s')]), transition('open <=> closed', [animate('0.5s')]), transition('* => open', [animate('1s', style({opacity: '*'}))]), transition('* => *', [animate('1s')]), ]), ], templateUrl: 'open-close.component.html', styleUrls: ['open-close.component.css'],})export class OpenCloseComponent { @Input() logging = false; isOpen = true; toggle() { this.isOpen = !this.isOpen; } onAnimationEvent(event: AnimationEvent) { if (!this.logging) { return; } // openClose is trigger name in this example console.warn(`Animation Trigger: ${event.triggerName}`); // phaseName is "start" or "done" console.warn(`Phase: ${event.phaseName}`); // in our example, totalTime is 1000 (number of milliseconds in a second) console.warn(`Total time: ${event.totalTime}`); // in our example, fromState is either "open" or "closed" console.warn(`From: ${event.fromState}`); // in our example, toState either "open" or "closed" console.warn(`To: ${event.toState}`); // the HTML element itself, the button in this case console.warn(`Element: ${event.element}`); }}
Keyframes
To create an animation with multiple steps run in sequence, use keyframes.
Angular's keyframe()
function allows several style changes within a single timing segment.
For example, the button, instead of fading, could change color several times over a single 2-second time span.
The code for this color change might look like this.
src/app/status-slider.component.ts
import {Component} from '@angular/core';import {trigger, transition, state, animate, style, keyframes} from '@angular/animations';@Component({ selector: 'app-status-slider', templateUrl: 'status-slider.component.html', styleUrls: ['status-slider.component.css'], animations: [ trigger('slideStatus', [ state('inactive', style({backgroundColor: 'blue'})), state('active', style({backgroundColor: '#754600'})), transition('* => active', [ animate( '2s', keyframes([ style({backgroundColor: 'blue', offset: 0}), style({backgroundColor: 'red', offset: 0.8}), style({backgroundColor: '#754600', offset: 1.0}), ]), ), ]), transition('* => inactive', [ animate( '2s', keyframes([ style({backgroundColor: '#754600', offset: 0}), style({backgroundColor: 'red', offset: 0.2}), style({backgroundColor: 'blue', offset: 1.0}), ]), ), ]), transition('* => active', [ animate( '2s', keyframes([ style({backgroundColor: 'blue'}), style({backgroundColor: 'red'}), style({backgroundColor: 'orange'}), ]), ), ]), ]), ],})export class StatusSliderComponent { status: 'active' | 'inactive' = 'inactive'; toggle() { if (this.status === 'active') { this.status = 'inactive'; } else { this.status = 'active'; } }}
Offset
Keyframes include an offset
that defines the point in the animation where each style change occurs.
Offsets are relative measures from zero to one, marking the beginning and end of the animation. They should be applied to each of the keyframe steps if used at least once.
Defining offsets for keyframes is optional. If you omit them, evenly spaced offsets are automatically assigned. For example, three keyframes without predefined offsets receive offsets of 0, 0.5, and 1. Specifying an offset of 0.8 for the middle transition in the preceding example might look like this.
The code with offsets specified would be as follows.
src/app/status-slider.component.ts
import {Component} from '@angular/core';import {trigger, transition, state, animate, style, keyframes} from '@angular/animations';@Component({ selector: 'app-status-slider', templateUrl: 'status-slider.component.html', styleUrls: ['status-slider.component.css'], animations: [ trigger('slideStatus', [ state('inactive', style({backgroundColor: 'blue'})), state('active', style({backgroundColor: '#754600'})), transition('* => active', [ animate( '2s', keyframes([ style({backgroundColor: 'blue', offset: 0}), style({backgroundColor: 'red', offset: 0.8}), style({backgroundColor: '#754600', offset: 1.0}), ]), ), ]), transition('* => inactive', [ animate( '2s', keyframes([ style({backgroundColor: '#754600', offset: 0}), style({backgroundColor: 'red', offset: 0.2}), style({backgroundColor: 'blue', offset: 1.0}), ]), ), ]), transition('* => active', [ animate( '2s', keyframes([ style({backgroundColor: 'blue'}), style({backgroundColor: 'red'}), style({backgroundColor: 'orange'}), ]), ), ]), ]), ],})export class StatusSliderComponent { status: 'active' | 'inactive' = 'inactive'; toggle() { if (this.status === 'active') { this.status = 'inactive'; } else { this.status = 'active'; } }}
You can combine keyframes with duration
, delay
, and easing
within a single animation.
Keyframes with a pulsation
Use keyframes to create a pulse effect in your animations by defining styles at specific offset throughout the animation.
Here's an example of using keyframes to create a pulse effect:
- The original
open
andclosed
states, with the original changes in height, color, and opacity, occurring over a timeframe of 1 second - A keyframes sequence inserted in the middle that causes the button to appear to pulsate irregularly over the course of that same 1 second timeframe
The code snippet for this animation might look like this.
src/app/open-close.component.ts
import {Component, Input} from '@angular/core';import { trigger, transition, state, animate, style, keyframes, AnimationEvent,} from '@angular/animations';@Component({ selector: 'app-open-close', animations: [ trigger('openClose', [ state( 'open', style({ height: '200px', opacity: 1, backgroundColor: 'yellow', }), ), state( 'close', style({ height: '100px', opacity: 0.5, backgroundColor: 'green', }), ), // ... transition('* => *', [ animate( '1s', keyframes([ style({opacity: 0.1, offset: 0.1}), style({opacity: 0.6, offset: 0.2}), style({opacity: 1, offset: 0.5}), style({opacity: 0.2, offset: 0.7}), ]), ), ]), ]), ], templateUrl: 'open-close.component.html', styleUrls: ['open-close.component.css'],})export class OpenCloseKeyframeComponent { isOpen = false; toggle() { this.isOpen = !this.isOpen; } @Input() logging = false; onAnimationEvent(event: AnimationEvent) { if (!this.logging) { return; } }}
Animatable properties and units
Angular animations support builds on top of web animations, so you can animate any property that the browser considers animatable. This includes positions, sizes, transforms, colors, borders, and more. The W3C maintains a list of animatable properties on its CSS Transitions page.
For properties with a numeric value, define a unit by providing the value as a string, in quotes, with the appropriate suffix:
50 pixels:
'50px'
Relative font size:
'3em'
Percentage:
'100%'
You can also provide the value as a number. In such cases Angular assumes a default unit of pixels, or px
.
Expressing 50 pixels as 50
is the same as saying '50px'
.
HELPFUL: The string "50"
would instead not be considered valid).
Automatic property calculation with wildcards
Sometimes, the value of a dimensional style property isn't known until runtime. For example, elements often have widths and heights that depend on their content or the screen size. These properties are often challenging to animate using CSS.
In these cases, you can use a special wildcard *
property value under style()
. The value of that particular style property is computed at runtime and then plugged into the animation.
The following example has a trigger called shrinkOut
, used when an HTML element leaves the page.
The animation takes whatever height the element has before it leaves, and animates from that height to zero.
src/app/hero-list-auto.component.ts
import {Component, Input, Output, EventEmitter} from '@angular/core';import {trigger, state, style, animate, transition} from '@angular/animations';import {Hero} from './hero';import {NgFor} from '@angular/common';@Component({ selector: 'app-hero-list-auto', templateUrl: 'hero-list-auto.component.html', styleUrls: ['./hero-list-page.component.css'], imports: [NgFor], animations: [ trigger('shrinkOut', [ state('in', style({height: '*'})), transition('* => void', [style({height: '*'}), animate(250, style({height: 0}))]), ]), ],})export class HeroListAutoComponent { @Input() heroes: Hero[] = []; @Output() remove = new EventEmitter<number>(); removeHero(id: number) { this.remove.emit(id); }}
Keyframes summary
The keyframes()
function in Angular allows you to specify multiple interim styles within a single transition. An optional offset
can be used to define the point in the animation where each style change should occur.
More on Angular animations
You might also be interested in the following: