Animation (Comprehensive Guide)

A comprehensive developer guide for implementing animations in Aurelia applications, covering CSS animations, Web Animations API, lifecycle hooks, third-party libraries, and advanced animation pattern

Master animation techniques in Aurelia applications, from simple CSS transitions to complex coordinated animations using lifecycle hooks, modern Web APIs, and third-party libraries.

Overview

Aurelia provides multiple approaches for implementing animations, each suited to different use cases:

Approach
Best For
Complexity
Performance

CSS Transitions

Simple state changes, hover effects

Low

Excellent

CSS Animations

Loading spinners, attention-grabbers

Low

Excellent

Web Animations API

Programmatic control, complex sequences

Medium

Excellent

Lifecycle Hooks

Component mount/unmount, route transitions

Medium

Excellent

Third-party Libraries

Advanced effects, timelines, physics

High

Good

When to Use What

Choose CSS Transitions When:

  • Animating between two states (expanded/collapsed, visible/hidden)

  • Creating hover effects or focus states

  • You need the best performance with minimal code

Choose CSS Keyframe Animations When:

  • Creating looping animations (spinners, pulsing effects)

  • Needing multi-step animations that don't respond to state

  • Building simple attention-grabbing effects

Choose Web Animations API When:

  • You need JavaScript control over CSS-quality animations

  • Building interactive animations that respond to user input

  • Coordinating multiple animations programmatically

  • Animations need to be paused, reversed, or dynamically adjusted

Choose Lifecycle Hooks When:

  • Animating component entrance/exit

  • Coordinating animations with component lifecycle

  • Creating route transition effects

  • Animations depend on DOM measurements

Choose Third-party Libraries When:

  • You need advanced easing functions or physics

  • Building complex animation timelines

  • Implementing scroll-triggered animations

  • Need SVG morphing or path animations

CSS-Based Animations

Simple State Transitions

CSS transitions are perfect for smooth state changes:

export class ToggleCard {
  private isExpanded = false;

  toggle() {
    this.isExpanded = !this.isExpanded;
  }
}
.card {
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  max-height: 100px;
  overflow: hidden;
  background: #f5f5f5;
  padding: 20px;
  border-radius: 8px;
  cursor: pointer;
}

.card.expanded {
  max-height: 500px;
  background: #e3f2fd;
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}

.card-content {
  transition: opacity 0.2s ease-in-out;
  opacity: 0;
}

.card.expanded .card-content {
  opacity: 1;
  transition-delay: 0.1s;
}
<div class="card" class.bind="isExpanded ? 'expanded' : ''" click.trigger="toggle()">
  <h3>Click to ${isExpanded ? 'collapse' : 'expand'}</h3>
  <div class="card-content">
    <p>This content animates in when the card expands.</p>
  </div>
</div>

Multi-Step Keyframe Animations

For more complex animations, use @keyframes:

export class AttentionButton {
  private isAnimating = false;

  animateButton() {
    this.isAnimating = true;
    setTimeout(() => {
      this.isAnimating = false;
    }, 2000);
  }
}
@keyframes wiggle {
  0%, 7% { transform: rotateZ(0); }
  15% { transform: rotateZ(-15deg); }
  20% { transform: rotateZ(10deg); }
  25% { transform: rotateZ(-10deg); }
  30% { transform: rotateZ(6deg); }
  35% { transform: rotateZ(-4deg); }
  40%, 100% { transform: rotateZ(0); }
}

.wiggle {
  animation: wiggle 2s linear 1;
}
<button
  type="button"
  class.bind="isAnimating ? 'wiggle' : ''"
  disabled.bind="isAnimating"
  click.trigger="animateButton()">
  ${isAnimating ? 'Wiggling...' : 'Click to Wiggle!'}
</button>

Staggered List Animations

Animate list items with a stagger effect using CSS custom properties:

export class StaggeredList {
  private items = [
    'First item',
    'Second item',
    'Third item',
    'Fourth item',
    'Fifth item'
  ];
}
<ul class="staggered-list">
  <li repeat.for="item of items" class="list-item" css="--index: ${$index}">
    ${item}
  </li>
</ul>
.list-item {
  opacity: 0;
  animation: fadeInSlide 0.5s ease-out forwards;
  animation-delay: calc(var(--index) * 0.1s);
}

@keyframes fadeInSlide {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

Web Animations API

The Web Animations API provides programmatic control over animations with excellent performance.

Basic Web Animations

export class WebAnimationExample {
  private element: HTMLElement;

  created(controller) {
    this.element = controller.host;
  }

  async animateElement(): Promise<void> {
    const animation = this.element.animate([
      { transform: 'scale(1) rotate(0deg)', opacity: 1 },
      { transform: 'scale(1.2) rotate(180deg)', opacity: 0.7 },
      { transform: 'scale(1) rotate(360deg)', opacity: 1 }
    ], {
      duration: 1000,
      easing: 'ease-in-out'
    });

    await animation.finished;
    console.log('Animation completed!');
  }
}

Interactive Animations

Create animations that respond to user input:

export class InteractiveAnimation {
  private x = 0;
  private y = 0;

  mouseMove(event: MouseEvent) {
    this.x = event.clientX;
    this.y = event.clientY;
  }
}
<div
  mousemove.trigger="mouseMove($event)"
  style="background-color: hsl(${x / 3}, 60%, 45%)"
  class="interactive-area">
  <h3>Interactive Color Animation</h3>
  <p>Move your mouse to change the background color!</p>
  <p>X: ${x}, Y: ${y}</p>
</div>
.interactive-area {
  padding: 40px;
  transition: 0.3s background-color ease-in-out;
  border-radius: 8px;
  cursor: crosshair;
  min-height: 200px;
  color: white;
  text-align: center;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

Controllable Animations

Create animations you can pause, play, or reverse:

export class ControllableAnimation {
  private element: HTMLElement;
  private animation: Animation | null = null;

  created(controller) {
    this.element = controller.host;
  }

  private createAnimation(): Animation {
    return this.element.animate([
      { transform: 'translateX(0)' },
      { transform: 'translateX(300px)' }
    ], {
      duration: 2000,
      easing: 'ease-in-out',
      fill: 'both'
    });
  }

  play() {
    if (!this.animation) {
      this.animation = this.createAnimation();
    }
    this.animation.play();
  }

  pause() {
    this.animation?.pause();
  }

  reverse() {
    if (this.animation) {
      this.animation.reverse();
    }
  }

  reset() {
    this.animation?.cancel();
    this.animation = null;
  }
}
<div ref="element" class="animated-box">Animated Box</div>
<div class="controls">
  <button click.trigger="play()">Play</button>
  <button click.trigger="pause()">Pause</button>
  <button click.trigger="reverse()">Reverse</button>
  <button click.trigger="reset()">Reset</button>
</div>

Coordinated Animations

Run multiple animations simultaneously:

export class CoordinatedAnimations {
  private element: HTMLElement;

  created(controller) {
    this.element = controller.host;
  }

  async animateWithCoordination(): Promise<void> {
    const background = this.element.animate([
      { backgroundColor: '#2196F3' },
      { backgroundColor: '#4CAF50' }
    ], { duration: 1000, fill: 'forwards' });

    const scale = this.element.animate([
      { transform: 'scale(1)' },
      { transform: 'scale(1.2)' },
      { transform: 'scale(1)' }
    ], { duration: 1000 });

    // Wait for both to complete
    await Promise.all([background.finished, scale.finished]);
  }
}

Component Lifecycle Animations

Animate components during their attachment and detachment using lifecycle hooks.

Fade In/Out Animation

export class FadeCard {
  private element: HTMLElement;

  created(controller) {
    this.element = controller.host;
  }

  attaching(): Promise<void> {
    this.element.style.opacity = '0';

    const animation = this.element.animate([
      { opacity: 0, transform: 'translateY(20px)' },
      { opacity: 1, transform: 'translateY(0)' }
    ], {
      duration: 400,
      easing: 'ease-out',
      fill: 'forwards'
    });

    return animation.finished;
  }

  detaching(): Promise<void> {
    const animation = this.element.animate([
      { opacity: 1, transform: 'translateY(0)' },
      { opacity: 0, transform: 'translateY(-20px)' }
    ], {
      duration: 300,
      easing: 'ease-in',
      fill: 'forwards'
    });

    return animation.finished;
  }
}

Reusable Animation Hooks

Create reusable animation controllers with @lifecycleHooks():

import { lifecycleHooks } from '@aurelia/runtime-html';

@lifecycleHooks()
export class FadeAnimationHooks {
  private element: HTMLElement;

  created(vm, controller): void {
    this.element = controller.host;
  }

  attaching(vm): Promise<void> {
    return this.element.animate([
      { opacity: 0, transform: 'scale(0.8)' },
      { opacity: 1, transform: 'scale(1)' }
    ], {
      duration: 300,
      easing: 'ease-out',
      fill: 'forwards'
    }).finished;
  }

  detaching(vm): Promise<void> {
    return this.element.animate([
      { opacity: 1, transform: 'scale(1)' },
      { opacity: 0, transform: 'scale(0.8)' }
    ], {
      duration: 200,
      easing: 'ease-in',
      fill: 'forwards'
    }).finished;
  }
}

Register globally in your app configuration:

import Aurelia from 'aurelia';
import { FadeAnimationHooks } from './fade-animation-hooks';

Aurelia
  .register(FadeAnimationHooks)
  .app(MyApp)
  .start();

Or use per-component:

export class MyComponent {
  static dependencies = [FadeAnimationHooks];
}

Animating Conditional Content

Animate elements controlled by if.bind:

@lifecycleHooks()
export class ConditionalAnimationHooks {
  private element: HTMLElement;

  created(vm, controller): void {
    this.element = controller.host;
  }

  attaching(vm): Promise<void> {
    this.element.style.opacity = '0';
    return this.element.animate([
      { opacity: 0, transform: 'translateY(-10px)' },
      { opacity: 1, transform: 'translateY(0)' }
    ], {
      duration: 250,
      easing: 'ease-out',
      fill: 'forwards'
    }).finished;
  }

  detaching(vm): Promise<void> {
    return this.element.animate([
      { opacity: 1, transform: 'translateY(0)' },
      { opacity: 0, transform: 'translateY(-10px)' }
    ], {
      duration: 200,
      easing: 'ease-in',
      fill: 'forwards'
    }).finished;
  }
}
<div if.bind="showContent">Content that animates in/out</div>

Animating List Items

Animate individual items in a repeat.for:

export class ListItem {
  private element: HTMLElement;

  created(controller) {
    this.element = controller.host;
  }

  attaching(): Promise<void> {
    return this.element.animate([
      { opacity: 0, transform: 'translateX(-20px) scale(0.8)' },
      { opacity: 1, transform: 'translateX(0) scale(1)' }
    ], {
      duration: 300,
      easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
      fill: 'forwards'
    }).finished;
  }

  detaching(): Promise<void> {
    return this.element.animate([
      { opacity: 1, transform: 'translateX(0) scale(1)' },
      { opacity: 0, transform: 'translateX(20px) scale(0.8)' }
    ], {
      duration: 250,
      easing: 'ease-in',
      fill: 'forwards'
    }).finished;
  }
}
<list-item repeat.for="item of items" model.bind="item"></list-item>

Router Transition Animations

Animate page transitions using router lifecycle hooks. These techniques work with both @aurelia/router (recommended) and router-direct. See the dedicated Router Animation Guide for comprehensive coverage.

Quick Router Animation Example

import { lifecycleHooks } from '@aurelia/runtime-html';

@lifecycleHooks()
export class SlideAnimationHooks {
  private element: HTMLElement;
  private backwards = false;

  created(vm, controller): void {
    this.element = controller.host;
  }

  loading(vm, _params, _instruction, navigation) {
    this.backwards = navigation.navigation.back;
  }

  unloading(vm, _instruction, navigation) {
    this.backwards = navigation.navigation.back;
  }

  attaching(vm): Promise<void> {
    return this.slideIn(this.element, this.backwards ? 'left' : 'right');
  }

  detaching(vm): Promise<void> {
    return this.slideOut(this.element, this.backwards ? 'right' : 'left');
  }

  private slideIn(element: HTMLElement, from: 'left' | 'right'): Promise<void> {
    const animation = element.animate([
      { transform: `translateX(${from === 'left' ? '-' : ''}100%)` },
      { transform: 'translateX(0)' }
    ], { duration: 300, easing: 'ease-out', fill: 'forwards' });

    return animation.finished;
  }

  private slideOut(element: HTMLElement, to: 'left' | 'right'): Promise<void> {
    const animation = element.animate([
      { transform: 'translateX(0)' },
      { transform: `translateX(${to === 'left' ? '-' : ''}100%)` }
    ], { duration: 300, easing: 'ease-in', fill: 'forwards' });

    return animation.finished;
  }
}

View Transitions API

The modern View Transitions API provides smooth, cross-fade transitions between DOM states with minimal code.

Basic View Transitions

export class ViewTransitionExample {
  private items = ['Item 1', 'Item 2', 'Item 3'];

  async updateWithTransition() {
    // Check for browser support
    if (!document.startViewTransition) {
      // Fallback for browsers without support
      this.items.push(`Item ${this.items.length + 1}`);
      return;
    }

    // Start a view transition
    const transition = document.startViewTransition(() => {
      this.items.push(`Item ${this.items.length + 1}`);
    });

    await transition.finished;
  }
}

Customizing View Transitions

Control the transition with CSS:

/* Customize the transition animation */
::view-transition-old(root) {
  animation: fade-out 0.3s ease-out;
}

::view-transition-new(root) {
  animation: fade-in 0.3s ease-in;
}

@keyframes fade-out {
  to { opacity: 0; }
}

@keyframes fade-in {
  from { opacity: 0; }
}

Named View Transitions

Transition specific elements independently:

export class NamedTransitionExample {
  private selectedId: number | null = null;

  async selectItem(id: number) {
    if (!document.startViewTransition) {
      this.selectedId = id;
      return;
    }

    await document.startViewTransition(() => {
      this.selectedId = id;
    }).finished;
  }
}
<div repeat.for="item of items" click.trigger="selectItem(item.id)">
  <div css="view-transition-name: item-${item.id}">
    ${item.name}
  </div>
</div>

<div if.bind="selectedId" css="view-transition-name: item-${selectedId}">
  <h2>Selected Item Details</h2>
</div>
/* Animate the selected item's transition */
::view-transition-old(item-*),
::view-transition-new(item-*) {
  animation-duration: 0.5s;
}

Third-Party Animation Libraries

Aurelia works seamlessly with popular animation libraries.

Anime.js Integration

Anime.js provides powerful, lightweight animations:

import anime from 'animejs';

export class AnimeExample {
  private currentValue = 0;
  private displayValue = 0;
  private valueWrapper: HTMLElement;

  animateToValue(newValue: number) {
    this.currentValue = newValue;

    anime({
      targets: this,
      displayValue: newValue,
      easing: 'easeInOutQuart',
      round: true,
      duration: 1200,
      update: () => {
        this.valueWrapper.textContent = this.displayValue.toLocaleString();
      }
    });
  }

  increment() {
    this.animateToValue(this.currentValue + Math.floor(Math.random() * 10000));
  }

  reset() {
    this.animateToValue(0);
  }
}
<div class="value-animator">
  <h3>Animated Counter</h3>
  <div ref="valueWrapper" class="display-value">${displayValue & oneTime}</div>
  <div class="controls">
    <button click.trigger="increment()">Add Random Amount</button>
    <button click.trigger="reset()">Reset</button>
  </div>
</div>

GSAP Integration

GSAP provides professional-grade animation capabilities:

import { gsap } from 'gsap';

export class GSAPExample {
  private timeline: GSAPTimeline;

  attached() {
    this.timeline = gsap.timeline({ paused: true });

    this.timeline
      .from('.card', { y: 50, opacity: 0, duration: 0.5 })
      .from('.card h3', { x: -30, opacity: 0, duration: 0.3 }, '-=0.2')
      .from('.card p', { y: 20, opacity: 0, stagger: 0.1, duration: 0.3 }, '-=0.1');
  }

  playAnimation() {
    this.timeline.play();
  }

  reverseAnimation() {
    this.timeline.reverse();
  }

  disposing() {
    this.timeline?.kill();
  }
}

Stagger Animations with GSAP

Create beautiful staggered animations:

import { gsap } from 'gsap';

export class StaggerExample {
  private items = Array.from({ length: 10 }, (_, i) => `Item ${i + 1}`);

  attached() {
    gsap.from('.list-item', {
      opacity: 0,
      y: 30,
      stagger: 0.1,
      duration: 0.5,
      ease: 'power2.out'
    });
  }
}
<ul>
  <li repeat.for="item of items" class="list-item">${item}</li>
</ul>

Advanced Patterns

Reusable Animation Composer

Create a library of reusable animations:

export class AnimationComposer {
  static fadeIn(element: Element, duration = 300): Promise<void> {
    return element.animate([
      { opacity: 0 },
      { opacity: 1 }
    ], { duration, easing: 'ease-out', fill: 'forwards' }).finished;
  }

  static fadeOut(element: Element, duration = 300): Promise<void> {
    return element.animate([
      { opacity: 1 },
      { opacity: 0 }
    ], { duration, easing: 'ease-in', fill: 'forwards' }).finished;
  }

  static slideUp(element: Element, duration = 300): Promise<void> {
    return element.animate([
      { transform: 'translateY(20px)' },
      { transform: 'translateY(0)' }
    ], { duration, easing: 'ease-out', fill: 'forwards' }).finished;
  }

  static slideDown(element: Element, duration = 300): Promise<void> {
    return element.animate([
      { transform: 'translateY(0)' },
      { transform: 'translateY(20px)' }
    ], { duration, easing: 'ease-in', fill: 'forwards' }).finished;
  }

  static async fadeInAndSlideUp(element: Element): Promise<void> {
    await Promise.all([
      this.fadeIn(element),
      this.slideUp(element)
    ]);
  }

  static async fadeOutAndSlideDown(element: Element): Promise<void> {
    await Promise.all([
      this.fadeOut(element),
      this.slideDown(element)
    ]);
  }
}

Usage in components:

import { AnimationComposer } from './animation-composer';

export class MyComponent {
  private element: HTMLElement;

  created(controller) {
    this.element = controller.host;
  }

  attaching(): Promise<void> {
    return AnimationComposer.fadeInAndSlideUp(this.element);
  }

  detaching(): Promise<void> {
    return AnimationComposer.fadeOutAndSlideDown(this.element);
  }
}

Sequential Animations

Chain animations together in sequence:

export class SequenceAnimation {
  async playSequence(elements: Element[]): Promise<void> {
    for (const [index, element] of elements.entries()) {
      await element.animate([
        { opacity: 0, transform: 'translateX(-20px)' },
        { opacity: 1, transform: 'translateX(0)' }
      ], {
        duration: 200,
        delay: index * 100,
        easing: 'ease-out',
        fill: 'forwards'
      }).finished;
    }
  }
}

Parallel Animation Groups

Run groups of animations simultaneously:

export class ParallelAnimations {
  async animateGroup(elements: Element[]): Promise<void> {
    const animations = elements.map(element =>
      element.animate([
        { opacity: 0, transform: 'scale(0.8)' },
        { opacity: 1, transform: 'scale(1)' }
      ], {
        duration: 300,
        easing: 'ease-out',
        fill: 'forwards'
      }).finished
    );

    await Promise.all(animations);
  }
}

Scroll-Triggered Animations

Animate elements as they enter the viewport:

export class ScrollAnimation {
  private observer: IntersectionObserver;
  private elements: Element[] = [];

  attached() {
    this.observer = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            this.animateElement(entry.target);
            this.observer.unobserve(entry.target);
          }
        });
      },
      { threshold: 0.1 }
    );

    this.elements.forEach(el => this.observer.observe(el));
  }

  private animateElement(element: Element): void {
    element.animate([
      { opacity: 0, transform: 'translateY(50px)' },
      { opacity: 1, transform: 'translateY(0)' }
    ], {
      duration: 600,
      easing: 'ease-out',
      fill: 'forwards'
    });
  }

  detaching() {
    this.observer?.disconnect();
  }
}

Performance Optimization

Use GPU-Accelerated Properties

Always prefer properties that can be animated on the GPU:

/* Good - GPU accelerated */
.animated {
  transform: translateX(100px) scale(1.2);
  opacity: 0.5;
}

/* Avoid - triggers layout recalculation */
.animated {
  left: 100px;
  width: 200px;
  margin-top: 50px;
}

Hint the Browser with will-change

For complex animations, hint what will change:

.complex-animation {
  will-change: transform, opacity;
}

/* Remove will-change after animation completes */
.complex-animation.done {
  will-change: auto;
}

Clean Up Animations

Always cancel animations when components are destroyed:

export class AnimatedComponent {
  private activeAnimations: Animation[] = [];

  startAnimation() {
    const animation = this.element.animate(/* ... */);
    this.activeAnimations.push(animation);
  }

  disposing() {
    this.activeAnimations.forEach(animation => animation.cancel());
    this.activeAnimations = [];
  }
}

Batch DOM Operations

Minimize layout thrashing by batching DOM reads and writes:

export class BatchedAnimation {
  async animateElements(elements: Element[]): Promise<void> {
    // Read phase - batch all measurements
    const positions = elements.map(el => ({
      element: el,
      rect: el.getBoundingClientRect()
    }));

    // Write phase - batch all animations
    const animations = positions.map(({ element, rect }) =>
      element.animate([
        { transform: `translateY(${rect.height}px)` },
        { transform: 'translateY(0)' }
      ], { duration: 300 }).finished
    );

    await Promise.all(animations);
  }
}

Accessibility Considerations

Respect prefers-reduced-motion

Always respect user preferences for reduced motion:

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

In JavaScript:

export class AccessibleAnimation {
  private shouldAnimate(): boolean {
    return !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  }

  animateIfAllowed(element: Element): Promise<void> | void {
    if (!this.shouldAnimate()) {
      return Promise.resolve();
    }

    const animation = element.animate([
      { opacity: 0 },
      { opacity: 1 }
    ], { duration: 300 });

    return animation.finished;
  }
}

Maintain Focus Management

Ensure focus remains intuitive during animations:

export class FocusAwareAnimation {
  async animateAndFocus(element: HTMLElement): Promise<void> {
    await element.animate([
      { opacity: 0, transform: 'scale(0.8)' },
      { opacity: 1, transform: 'scale(1)' }
    ], { duration: 200 }).finished;

    // Focus after animation completes
    if (element.tabIndex >= 0 || element.matches('button, a, input, select, textarea')) {
      element.focus();
    }
  }
}

Provide Skip Options

For long or complex animations, provide a way to skip:

export class SkippableAnimation {
  private skipRequested = false;

  async playAnimation(): Promise<void> {
    const animation = this.element.animate(/* ... */, { duration: 2000 });

    // Allow skipping
    const skipHandler = () => {
      this.skipRequested = true;
      animation.finish();
    };

    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape') skipHandler();
    }, { once: true });

    await animation.finished;
  }
}

Lifecycle Hook Reference

Understanding lifecycle hooks is crucial for timing animations correctly:

Hook
When Called
Best For
Return Promise?

created

After construction

Element reference setup

No

binding

Before data binding

Pre-animation state setup

No

bound

After data binding

Data-dependent setup

No

attaching

Before DOM insertion

Enter animations

Yes

attached

After DOM insertion

Animations needing measurements

No

detaching

Before DOM removal

Exit animations

Yes

unbinding

Before unbinding

Cleanup

No

disposing

Before disposal

Cancel active animations

No

Best Practices for Lifecycle Animations

  1. Always return promises from attaching and detaching:

    attaching(): Promise<void> {
      return this.element.animate(/* ... */).finished;
    }
  2. Handle interruptions - Cancel animations if detached early:

    export class AnimatedComponent {
      private currentAnimation: Animation | null = null;
    
      attaching(): Promise<void> {
        this.currentAnimation = this.element.animate(/* ... */);
        return this.currentAnimation.finished;
      }
    
      detaching(): Promise<void> {
        this.currentAnimation?.cancel();
        this.currentAnimation = this.element.animate(/* ... */);
        return this.currentAnimation.finished;
      }
    }
  3. Coordinate complex animations:

    attaching(): Promise<void> {
      return Promise.all([
        this.animateBackground(),
        this.animateContent(),
        this.animateControls()
      ]).then(() => void 0);
    }

Real-World Examples

Notification Toast

export class ToastNotification {
  private element: HTMLElement;
  private message = '';
  private visible = false;

  created(controller) {
    this.element = controller.host;
  }

  async show(message: string, duration = 3000): Promise<void> {
    this.message = message;
    this.visible = true;

    // Slide in
    await this.element.animate([
      { transform: 'translateY(-100%)', opacity: 0 },
      { transform: 'translateY(0)', opacity: 1 }
    ], { duration: 300, easing: 'ease-out', fill: 'forwards' }).finished;

    // Wait
    await new Promise(resolve => setTimeout(resolve, duration));

    // Slide out
    await this.element.animate([
      { transform: 'translateY(0)', opacity: 1 },
      { transform: 'translateY(-100%)', opacity: 0 }
    ], { duration: 200, easing: 'ease-in', fill: 'forwards' }).finished;

    this.visible = false;
  }
}
export class ModalDialog {
  private backdrop: HTMLElement;
  private dialog: HTMLElement;
  private isOpen = false;

  async open(): Promise<void> {
    this.isOpen = true;

    // Animate backdrop and dialog in parallel
    await Promise.all([
      this.backdrop.animate([
        { opacity: 0 },
        { opacity: 1 }
      ], { duration: 200, fill: 'forwards' }).finished,

      this.dialog.animate([
        { opacity: 0, transform: 'scale(0.7) translateY(-50px)' },
        { opacity: 1, transform: 'scale(1) translateY(0)' }
      ], { duration: 300, easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)', fill: 'forwards' }).finished
    ]);
  }

  async close(): Promise<void> {
    await Promise.all([
      this.backdrop.animate([
        { opacity: 1 },
        { opacity: 0 }
      ], { duration: 200, fill: 'forwards' }).finished,

      this.dialog.animate([
        { opacity: 1, transform: 'scale(1) translateY(0)' },
        { opacity: 0, transform: 'scale(0.7) translateY(-50px)' }
      ], { duration: 200, easing: 'ease-in', fill: 'forwards' }).finished
    ]);

    this.isOpen = false;
  }
}
export class ImageGallery {
  private images = ['img1.jpg', 'img2.jpg', 'img3.jpg'];
  private currentIndex = 0;
  private imageElement: HTMLElement;

  async nextImage(): Promise<void> {
    const nextIndex = (this.currentIndex + 1) % this.images.length;

    // Fade out current
    await this.imageElement.animate([
      { opacity: 1 },
      { opacity: 0 }
    ], { duration: 200, easing: 'ease-in', fill: 'forwards' }).finished;

    // Change image
    this.currentIndex = nextIndex;

    // Fade in next
    await this.imageElement.animate([
      { opacity: 0 },
      { opacity: 1 }
    ], { duration: 200, easing: 'ease-out', fill: 'forwards' }).finished;
  }
}

Summary

Aurelia provides a flexible animation system that works with:

  • CSS for simple, performant animations

  • Web Animations API for programmatic control

  • Lifecycle hooks for component and route transitions

  • Third-party libraries for advanced effects

Choose the right tool for your needs, prioritize performance, and always consider accessibility. For router-specific animations (works with both @aurelia/router and router-direct), see the Router Animation Guide.

Last updated

Was this helpful?