System for creating and using reusable animation definitions that can be shared across multiple components and customized with parameters.
Creates a reusable animation definition that can be used with useAnimation().
/**
* Creates reusable animation for use with useAnimation()
* @param steps - Animation steps or single animation metadata
* @param options - Optional default parameters and timing
* @returns AnimationReferenceMetadata for reuse across components
*/
function animation(
steps: AnimationMetadata | AnimationMetadata[],
options?: AnimationOptions | null
): AnimationReferenceMetadata;
interface AnimationReferenceMetadata extends AnimationMetadata {
animation: AnimationMetadata | AnimationMetadata[];
options: AnimationOptions | null;
}Usage Examples:
import { animation, animate, style, keyframes } from '@angular/animations';
// Simple reusable animation
export const fadeInAnimation = animation([
style({ opacity: 0 }),
animate('{{ duration }}ms {{ easing }}', style({ opacity: 1 }))
], { params: { duration: 300, easing: 'ease-in' } });
// Complex reusable animation with keyframes
export const bounceInAnimation = animation([
animate('{{ duration }}ms {{ easing }}', keyframes([
style({ opacity: 0, transform: 'scale(0.3)', offset: 0 }),
style({ opacity: 1, transform: 'scale(1.05)', offset: 0.5 }),
style({ opacity: 1, transform: 'scale(0.9)', offset: 0.7 }),
style({ opacity: 1, transform: 'scale(1)', offset: 1 })
]))
], { params: { duration: 600, easing: 'ease-out' } });
// Multi-step reusable animation
export const slideAndFadeAnimation = animation([
style({
opacity: 0,
transform: 'translateX({{ startX }}px)'
}),
animate('{{ duration }}ms {{ easing }}', style({
opacity: 1,
transform: 'translateX(0)'
}))
], { params: { startX: -100, duration: 400, easing: 'ease-out' } });Uses a reusable animation created with animation(), optionally overriding parameters.
/**
* Uses a reusable animation with optional parameter overrides
* @param animation - AnimationReferenceMetadata from animation() function
* @param options - Optional parameter overrides and timing
* @returns AnimationAnimateRefMetadata for using the reusable animation
*/
function useAnimation(
animation: AnimationReferenceMetadata,
options?: AnimationOptions | null
): AnimationAnimateRefMetadata;
interface AnimationAnimateRefMetadata extends AnimationMetadata {
animation: AnimationReferenceMetadata;
options: AnimationOptions | null;
}Usage Examples:
import { trigger, transition, useAnimation } from '@angular/animations';
import { fadeInAnimation, bounceInAnimation, slideAndFadeAnimation } from './animations';
// Use animation with default parameters
trigger('fadeIn', [
transition(':enter', [
useAnimation(fadeInAnimation)
])
])
// Use animation with parameter overrides
trigger('customFade', [
transition(':enter', [
useAnimation(fadeInAnimation, {
params: { duration: 500, easing: 'ease-in-out' }
})
])
])
// Use animation with different parameters per transition
trigger('slideVariations', [
transition('* => left', [
useAnimation(slideAndFadeAnimation, {
params: { startX: -200, duration: 300 }
})
]),
transition('* => right', [
useAnimation(slideAndFadeAnimation, {
params: { startX: 200, duration: 300 }
})
]),
transition('* => slow', [
useAnimation(slideAndFadeAnimation, {
params: { startX: -100, duration: 800, easing: 'ease-in-out' }
})
])
])Runs child animations when parent animation executes, useful for coordinating nested animations.
/**
* Runs child animations when parent animation executes
* @param options - Optional duration and parameter configuration
* @returns AnimationAnimateChildMetadata for child animation coordination
*/
function animateChild(
options?: AnimateChildOptions | null
): AnimationAnimateChildMetadata;
interface AnimateChildOptions extends AnimationOptions {
duration?: number | string;
}
interface AnimationAnimateChildMetadata extends AnimationMetadata {
options: AnimateChildOptions | null;
}Usage Examples:
import { trigger, transition, animateChild, group, query } from '@angular/animations';
// Basic child animation coordination
trigger('parentAnimation', [
transition('* => *', [
group([
animate('300ms', style({ opacity: 0.5 })), // Parent animation
animateChild() // Run any child animations simultaneously
])
])
])
// Child animation with custom duration
trigger('timedChildAnimation', [
transition(':enter', [
group([
animate('500ms', style({ transform: 'scale(1.1)' })),
animateChild({ duration: '300ms' }) // Child animations run for 300ms
])
])
])
// Query with animateChild for specific child elements
trigger('coordinatedAnimation', [
transition('inactive => active', [
group([
// Parent element animation
animate('400ms', style({ backgroundColor: 'lightblue' })),
// Coordinate child animations
query('.child-element', [
animateChild({ duration: '400ms' })
], { optional: true })
])
])
])// animations.ts - Shared animation library
import { animation, animate, style, keyframes, group, sequence } from '@angular/animations';
// Entrance animations
export const fadeIn = animation([
style({ opacity: 0 }),
animate('{{ duration }}ms {{ easing }}', style({ opacity: 1 }))
], { params: { duration: 300, easing: 'ease-in' } });
export const slideInLeft = animation([
style({ transform: 'translateX(-100%)', opacity: 0 }),
animate('{{ duration }}ms {{ easing }}', style({
transform: 'translateX(0)',
opacity: 1
}))
], { params: { duration: 400, easing: 'ease-out' } });
export const bounceIn = animation([
animate('{{ duration }}ms {{ easing }}', keyframes([
style({ opacity: 0, transform: 'scale(0.3)', offset: 0 }),
style({ opacity: 1, transform: 'scale(1.05)', offset: 0.5 }),
style({ opacity: 1, transform: 'scale(0.9)', offset: 0.7 }),
style({ opacity: 1, transform: 'scale(1)', offset: 1 })
]))
], { params: { duration: 600, easing: 'ease-out' } });
// Exit animations
export const fadeOut = animation([
animate('{{ duration }}ms {{ easing }}', style({ opacity: 0 }))
], { params: { duration: 300, easing: 'ease-out' } });
export const slideOutRight = animation([
animate('{{ duration }}ms {{ easing }}', style({
transform: 'translateX(100%)',
opacity: 0
}))
], { params: { duration: 400, easing: 'ease-in' } });
// Complex animations
export const pulseAndScale = animation([
sequence([
animate('200ms', style({ transform: 'scale(1.1)' })),
animate('200ms', style({ transform: 'scale(1)' })),
animate('200ms', style({ transform: 'scale(1.1)' })),
animate('200ms', style({ transform: 'scale(1)' }))
])
]);
export const flipCard = animation([
group([
sequence([
animate('{{ duration }}ms ease-in', style({
transform: 'rotateY(90deg)'
})),
animate('{{ duration }}ms ease-out', style({
transform: 'rotateY(0deg)'
}))
])
])
], { params: { duration: 300 } });// component.ts - Using shared animations
import { Component } from '@angular/core';
import { trigger, transition, useAnimation } from '@angular/animations';
import {
fadeIn,
fadeOut,
slideInLeft,
slideOutRight,
bounceIn,
pulseAndScale
} from './animations';
@Component({
selector: 'app-card',
template: `
<div [@cardAnimation]="state" class="card">
<h3>{{ title }}</h3>
<p>{{ content }}</p>
</div>
`,
animations: [
trigger('cardAnimation', [
// Different entrance animations
transition('void => fadeIn', [
useAnimation(fadeIn)
]),
transition('void => slideIn', [
useAnimation(slideInLeft, {
params: { duration: 500, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)' }
})
]),
transition('void => bounceIn', [
useAnimation(bounceIn)
]),
// Exit animations
transition('* => void', [
useAnimation(fadeOut, {
params: { duration: 200 }
})
]),
// Interaction animations
transition('idle => pulse', [
useAnimation(pulseAndScale)
])
])
]
})
export class CardComponent {
state = 'fadeIn';
title = 'Card Title';
content = 'Card content...';
triggerPulse() {
this.state = 'pulse';
setTimeout(() => this.state = 'idle', 1000);
}
}// animation-factory.ts
import { animation, animate, style, sequence, group } from '@angular/animations';
export function createSlideAnimation(direction: 'left' | 'right' | 'up' | 'down') {
const transforms = {
left: 'translateX(-100%)',
right: 'translateX(100%)',
up: 'translateY(-100%)',
down: 'translateY(100%)'
};
return animation([
style({
transform: transforms[direction],
opacity: 0
}),
animate('{{ duration }}ms {{ easing }}', style({
transform: 'translate(0, 0)',
opacity: 1
}))
], { params: { duration: 400, easing: 'ease-out' } });
}
export function createScaleAnimation(fromScale: number = 0, toScale: number = 1) {
return animation([
style({
transform: `scale(${fromScale})`,
opacity: 0
}),
animate('{{ duration }}ms {{ easing }}', style({
transform: `scale(${toScale})`,
opacity: 1
}))
], { params: { duration: 300, easing: 'ease-out' } });
}
// Usage
const slideLeft = createSlideAnimation('left');
const slideUp = createSlideAnimation('up');
const scaleIn = createScaleAnimation(0.5, 1);
const scaleOut = createScaleAnimation(1, 0.8);export function createConditionalAnimation(includeRotation: boolean = false) {
const baseSteps = [
style({ opacity: 0, transform: 'translateY(20px)' }),
animate('300ms ease-out', style({ opacity: 1, transform: 'translateY(0)' }))
];
const rotationStep = animate('200ms', style({ transform: 'rotate(360deg)' }));
return animation(
includeRotation ? [...baseSteps, rotationStep] : baseSteps
);
}export const parentChildAnimation = animation([
group([
// Parent animation
sequence([
style({ opacity: 0, transform: 'scale(0.8)' }),
animate('300ms ease-out', style({ opacity: 1, transform: 'scale(1)' }))
]),
// Coordinate children
animateChild({ duration: '500ms' })
])
]);
// Usage in component
trigger('coordinated', [
transition(':enter', [
useAnimation(parentChildAnimation)
])
])// Good: Descriptive parameter names
animation([
animate('{{ enterDuration }}ms {{ enterEasing }}',
style({ opacity: '{{ targetOpacity }}' })
)
], {
params: {
enterDuration: 300,
enterEasing: 'ease-in',
targetOpacity: 1
}
});
// Avoid: Generic parameter names
animation([
animate('{{ time }}ms {{ curve }}', style({ opacity: '{{ value }}' }))
], { params: { time: 300, curve: 'ease-in', value: 1 } });// Always provide sensible defaults
export const flexibleAnimation = animation([
style({
opacity: '{{ startOpacity }}',
transform: 'translateX({{ startX }}px) scale({{ startScale }})'
}),
animate('{{ duration }}ms {{ easing }}', style({
opacity: '{{ endOpacity }}',
transform: 'translateX({{ endX }}px) scale({{ endScale }})'
}))
], {
params: {
startOpacity: 0,
endOpacity: 1,
startX: -50,
endX: 0,
startScale: 0.9,
endScale: 1,
duration: 400,
easing: 'ease-out'
}
});// Group related animations together
export const cardAnimations = {
enter: {
fade: fadeIn,
slide: slideInLeft,
bounce: bounceIn
},
exit: {
fade: fadeOut,
slide: slideOutRight
},
interactions: {
pulse: pulseAndScale,
flip: flipCard
}
};
// Usage
useAnimation(cardAnimations.enter.fade)
useAnimation(cardAnimations.interactions.pulse)