Skip to content
Documentation/Motion values and imperative animation

Search documentation

Type to search the documentation.

Guides

Motion values and imperative animation

Derive continuous motion from signals with moveValue, moveTransform and moveSpringValue, or animate an element you hold a reference to with MoveAnimator.

Not every animation is "play this when that happens". Two stable tools cover the rest.

Motion values are signals

moveValue holds a number, moveTransform maps it onto any output range, and moveSpringValue smooths a value with spring physics. They are plain signals, so they compose with computed and work under zoneless change detection.

meter.ts
@Component({
  selector: 'app-meter',
  template: `
    <input type="range" min="0" max="100" aria-label="Progress" (input)="set($event)" />
    <div [style.transform]="transform()">●</div>
  `,
})
export class Meter {
  /** The driver: any `Signal<number>` works; `moveValue` is a writable one. */
  protected readonly progress = moveValue(0);

  /** Map the driver onto whatever the UI needs. */
  protected readonly x = moveTransform(this.progress, [0, 100], [0, 260]);

  /** Let a spring smooth a value instead of following it rigidly. */
  protected readonly springX = moveSpringValue(this.x, { stiffness: 170, damping: 18 });

  protected readonly transform = computed(() => `translateX(${this.springX()}px)`);

  protected set(event: Event): void {
    this.progress.set(Number((event.target as HTMLInputElement).value));
  }
}

View source

moveTransform + moveSpringValue

Motion values are signals: the progress drives a derived position directly and a spring follows it.

progress 0.5 → x 110px · spring scale 1.05

Inputs

Component

const progress = signal(0.5); // any Signal<number>

const x = moveTransform(progress, [0, 1], [0, 220]);
const scale = moveSpringValue(moveTransform(progress, [0, 1], [0.8, 1.3]));

const transform = computed(() => `translateX(${x()}px) scale(${scale()})`);

moveTransform + moveSpringValue — expanded preview

Drag the progress control: the position is derived directly from the signal, and the spring follows it. Under reduced motion the spring jumps straight to its target. moveSpringValue creates an effect, so call it from an injection context (a field initialiser or constructor) or pass injector explicitly.

Open the full signals playground.

Imperative animation: MoveAnimator

When a handler or a service should animate an element it holds a reference to, inject MoveAnimator. It takes an Element or an ElementRef, handles reduced motion and server rendering for you, and its finished promise always resolves — unlike raw WAAPI, whose finished rejects on cancel.

toast-dismiss.ts
@Component({
  selector: 'app-toast-dismiss',
  template: `
    <div #toast>Saved</div>
    <button type="button" (click)="dismiss()">Dismiss</button>
  `,
})
export class ToastDismiss {
  readonly #animator = inject(MoveAnimator);
  protected readonly toast = viewChild.required<ElementRef<HTMLElement>>('toast');

  async dismiss(): Promise<void> {
    // `finished` always resolves: natural finish, cancel(), a second play(), or teardown.
    await this.#animator.animate(
      this.toast(),
      { opacity: [1, 0], y: [0, 12] },
      { duration: '180ms' },
    )?.finished;
    this.toast().nativeElement.remove();
  }
}

View source

pseudoElement animates ::view-transition-new(root) (or any pseudo-element) through the same API, which is how a View Transition reveal is written. Unsupported browsers get a no-op, and under reduced motion the pseudo-element animation is skipped while your DOM change still applies. See the imperative playground.

Choosing

  • A finite animation on an element you hold a reference to: MoveAnimator.
  • A continuous number you bind in the template (pointer position, scroll progress, a slider): motion values.

Related: moveValue playground · MoveAnimator playground · Patterns