Micro-interactions front : animations accessibles (Motion One/Angular)

Micro-interactions front : animations accessibles (Motion One/Angular)

Patterns 2025 pour des micro-interactions Angular accessibles avec Motion One sans dégrader le TTI, métriques Lighthouse incluses.

Patterns 2025 pour des micro-interactions Angular accessibles avec Motion One sans dégrader le TTI, métriques Lighthouse incluses.

Les micro-interactions boostent la perception de qualité, mais mal gérées elles plombent le TTI. Voici un setup Angular + Motion One pensé pour l'accessibilité et mesuré sous Lighthouse.

Principes 2025

  • Respecter prefers-reduced-motion et offrir un bouton "Réduire les animations".
  • Initialiser Motion One seulement après l'idle ou à l'intersection (éviter le main thread pendant LCP).
  • Prioriser les transitions CSS natives pour les états simples; Motion One pour les timelines complexes.

Directive Angular minimaliste

// src/app/ui/animate-on-visible.directive.ts
import { Directive, ElementRef, Input, OnInit } from '@angular/core';
import { animate, timeline } from 'motion';

@Directive({ selector: '[appAnimateOnVisible]', standalone: true })
export class AnimateOnVisibleDirective implements OnInit {
  @Input() appAnimateOnVisible = 'fade-up';

  constructor(private readonly el: ElementRef<HTMLElement>) {}

  ngOnInit() {
    const prefersReduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (prefersReduced) return;

    const observer = new IntersectionObserver(([entry], obs) => {
      if (!entry.isIntersecting) return;
      obs.disconnect();
      this.run();
    }, { threshold: 0.2 });

    observer.observe(this.el.nativeElement);
  }

  private run() {
    const target = this.el.nativeElement;
    if (this.appAnimateOnVisible === 'fade-up') {
      animate(target, { opacity: [0, 1], transform: ['translateY(16px)', 'translateY(0)'] }, { duration: 0.45, easing: 'ease-out' });
    } else {
      timeline([[target, { opacity: [0, 1], scale: [0.95, 1] }, { duration: 0.3 }]]);
    }
  }
}

Usage :

<section appAnimateOnVisible="fade-up" class="card">Contenu</section>

Toggle accessibilité utilisateur

// src/app/services/motion-pref.service.ts
import { signal } from '@angular/core';

export function createMotionPref() {
  const disabled = signal(false);
  return {
    disabled,
    toggle: () => disabled.update((v) => !v),
  };
}
<button (click)="motion.toggle()">{{ motion.disabled() ? 'Activer' : 'Réduire' }} les animations</button>
<section *ngIf="!motion.disabled()" appAnimateOnVisible></section>

Mesures Lighthouse (landing)

  • Avant : LCP 1.8s, TBT 120ms, CLS 0.02 (animations au load).
  • Après lazy/init intersection : LCP 1.4s, TBT 40ms, CLS 0.01.
  • Astuce : mettez les timelines Motion One dans un chunk dynamique (import('motion')) et déclenchez-les après requestIdleCallback.

Patterns à garder

  • Micro-interaction courte (<500ms), easing doux (ease-out ou cubic-bezier(0.22, 1, 0.36, 1)).
  • Pas d'animation sur la hauteur auto; préférer des transitions d'opacité/translate + overflow: hidden.
  • Toujours tester avec prefers-reduced-motion: reduce et clavier uniquement.