Skip to content

MoveAnimator

The imperative API: animate from TypeScript with the same keyframes, timing vocabulary and reduced-motion handling as the directives. Use it to sequence phases, react to a server response, or drive a View Transition. For a declarative relationship between two template elements, see moveTarget.

animate / set / clear

Order confirmed
checkout.tsts
import { Component, ElementRef, inject, viewChild } from '@angular/core';
import { MoveAnimator } from 'angular-movement';

@Component({ /* ... */ })
export class Checkout {
  readonly #animator = inject(MoveAnimator);
  readonly card = viewChild.required<ElementRef<HTMLElement>>('card');

  async confirm() {
    // ElementRef or Element — no .nativeElement needed.
    await this.#animator.animate(this.card(), { scale: [1, 1.04, 1] }, { duration: '240ms' })
      ?.finished;
    // finished always resolves, also when cancelled: no try/catch.
    this.#animator.set(this.card(), { opacity: 0.6, y: 8 }); // commit a state instantly
  }

  reset() {
    this.#animator.clear(this.card()); // back to the stylesheet
  }
}

View Transition reveal

pseudoElement lets the same API animate ::view-transition-new(root), so a theme reveal stays inside your motion system instead of dropping to raw WAAPI. Browsers without View Transitions simply swap instantly; under reduced motion the pseudo-element animation is skipped and the new state appears at once.

Daylight

last run: idle

theme-reveal.tsts
readonly #animator = inject(MoveAnimator);
readonly #document = inject(DOCUMENT);

reveal(event: MouseEvent, commit: () => void) {
  const doc = this.#document;
  if (!doc.startViewTransition) return commit(); // progressive enhancement

  const transition = doc.startViewTransition(commit);
  transition.ready.then(() => {
    const { clientX: x, clientY: y } = event;
    const radius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));

    this.#animator.animate(
      doc.documentElement,
      { clipPath: [`circle(0px at ${x}px ${y}px)`, `circle(${radius}px at ${x}px ${y}px)`] },
      {
        duration: '520ms',
        easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
        pseudoElement: '::view-transition-new(root)',
      },
    );
  });
}

/* global CSS — turn off the default cross-fade while this reveal runs */
::view-transition-old(root),
::view-transition-new(root) { animation: none; mix-blend-mode: normal; }

moveTrigger as a handle

flash.htmlhtml
<!-- A bare moveTrigger is an imperative handle: no dummy inputs. -->
<div #overlay="moveTrigger" moveTrigger></div>

// Two phases, two timings — per-call options, no input mutation.
await overlay.play({ opacity: [0, 1], scale: [0.6, 1] }, { duration: '420ms' });
await overlay.play({ opacity: [1, 0] }, { duration: '160ms', easing: 'ease-in' });