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.
@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));
}
}
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
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()})`);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.
@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();
}
}
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