
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-motionet 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èsrequestIdleCallback.
Patterns à garder
- Micro-interaction courte (<500ms), easing doux (
ease-outoucubic-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: reduceet clavier uniquement.
