Que fait motion ?
Cette compétence construit l'animation. Elle étend la section motion de product-ui/references/values.md, qui contient les chiffres arrêtés (courbes d'assouplissement, échelle de pression, rebond, décalage, le tableau des durées, durées des toasts et de mise en page, distances de déplacement, ratio sortie/entrée). Ces chiffres ne sont pas répétés ici ; cette compétence ajoute l'ordre des décisions, les tokens, les recettes et les modes d'échec.
Avant toute décision, appliquer product-ui §1 (le journal des goûts et la préséance) et §5 (quelle compétence possède quoi).
L'ordre des décisions
Parcourir ces étapes de haut en bas. Chaque étape peut terminer le travail. Zéro ligne d'animation est un résultat valide, et vous le dites.
1. Doit-il bouger ? Le filtre de fréquence
Demandez combien de fois par jour un seul utilisateur voit cet élément. Les niveaux sont dans values.md. En pratique :
- Initiated au clavier, ou vu 100+ fois par jour : pas d'animation. Pourquoi : le mouvement répété se transforme en délai.
- Vu des dizaines de fois par jour (survol, changement d'onglet, sélection de ligne) : rien, ou un changement de couleur instantané.
- Les données que l'utilisateur lit ou manipule (un nombre dans un tableau, un graphique dans un tableau de bord en direct) ne bougent jamais pour le style. Pourquoi : l'animation détourne l'œil de ce qui est lu.
- Si le filtre dit non, n'écrivez aucune animation. Dites quel niveau il a atteint et proposez l'alternative statique (un changement d'état instantané).
2. Nommer le but
Choisissez un mot : spatial (d'où il vient ou où il va), état (ceci a changé), retour (je vous ai entendu), continuité (empêche un saut), explication (moments rares uniquement). Impossible d'en nommer un : arrêtez. Pourquoi : « c'est joli » sur un élément fréquent est la principale source de sensation de lenteur dans l'interface.
3. Choisir l'outil : le moins cher qui fonctionne
| Besoin | Outil |
|---|---|
| Un état basculé par classe, attribut, survol ou pression | CSS transition |
Entrée depuis display: none, popover ou dialog | @starting-style + transition-behavior: allow-discrete |
| Une séquence unique, un décalage, ou une animation qui doit s'exécuter pendant que la page est occupée | CSS @keyframes |
JS décide quand ; vous avez besoin d'annuler, inverser, finished, ou FLIP | WAAPI el.animate() |
| Springs, transfert de vélocité, sortie d'un nœud React qui se démonte, mise en page partagée | Motion (motion/react) |
Si l'élément est un composant entier (menu, toast, tiroir), utilisez une bibliothèque qui gère le focus, et demandez pick-library. N'installez jamais Motion pour un fondu. Pourquoi : CSS s'exécute hors du thread principal et ne coûte rien en bundle.
4. Propriétés
- Animer
transformetopacity;filteruniquement pour un flou d'échange ≤ 2px. Nommer chaque propriété ; jamaistransition: all. - Changements de hauteur et de disposition : voir
references/recipes.md(lignes de grille, FLIP). Ne jamais animerwidth,top,leftoumarginpour déplacer quelque chose. Pourquoi : chacun force une mise en page à chaque image. - Ne jamais animer
box-shadow. Fondu de l'opacityd'un pseudo-élément à la place. - Les pourcentages
translatesont relatifs à la taille de l'élément lui-même, donctranslateY(100%)convient pour les tiroirs et les toasts de toute hauteur.
5. Courbe et durée, adaptées à l'usage
Ne choisissez jamais le nombre le plus proche de votre estimation. Indiquez l'usage (« fermeture de menu », « échange d'icône »), puis prenez cette ligne dans references/tokens.md. Si aucune ligne ne correspond à l'usage, ajoutez un nouveau token nommé d'après l'usage ; ne détournez pas un token existant. Pourquoi : un token est une déclaration sur le but de l'animation, donc un nombre proche avec le mauvais usage est le mauvais token.
- Entrée ou sortie :
--ease-out. Déplacement à l'écran :--ease-in-out. Tiroirs :--ease-drawer. Couleur :ease. Spinners :linear. Jamaisease-in. - Tout ce qui est déplaçable ou interruptible en plein vol utilise un spring, pas une courbe. Pourquoi : une courbe ne peut pas accepter la vélocité que la main de l'utilisateur avait.
- Sensation de lenteur : réduisez d'abord la durée, puis la distance, et seulement ensuite touchez à la courbe.
6. Interruption
- Tout ce que l'utilisateur peut déclencher deux fois par seconde est une transition ou un spring, jamais
@keyframes. Pourquoi : une transition recible depuis la valeur actuelle ; les keyframes redémarrent et sautent. - WAAPI : annulez l'ancienne animation après avoir lu la valeur actuelle, et démarrez la nouvelle à partir de celle-ci (
references/recipes.md). - Un interrupteur actionné 10 fois doit se terminer dans le bon état sans saut. Testez-le.
7. Sortie
- La sortie par défaut est un fondu, plus court que l'entrée (le fichier tokens a chaque paire).
- Tout ce qui a une direction (tiroir, panneau, toast glissé) part par où il est venu.
- L'élément qui sort cesse de prendre de l'espace immédiatement, donc les frères comblent l'écart pendant qu'il fond.
- Les sorties sont simultanées et jamais décalées. Pourquoi : l'utilisateur a déjà décidé de partir.
8. Mouvement réduit, livré avec l'animation
L'opacité et la couleur restent. Les diapositives, l'échelle, le flou et le rebond disparaissent ; les diapositives deviennent des fondus, le rebond devient 0. Ne jamais utiliser l'animation comme seul signal. Modèle dans references/recipes.md.
Chorégraphie
Classez chaque élément en mouvement avant de le chronométrer :
| Rôle | Traitement |
|---|---|
| Principal (la chose à remarquer) | Bouge en premier, parcourt le plus de distance |
| Support | Suit, parcourt moins |
| Décoration | Fondu sur place, ou rien |
| Statique (conteneurs, chrome) | Ne bouge jamais |
- Une entrée par conteneur. Si le panneau glisse, son contenu est déjà là. Pourquoi : deux entrées superposées font attendre l'utilisateur deux fois.
- Ordre par importance, pas par ordre DOM. Le délai reste aux étapes de values.md (40ms entre les éléments, 100ms entre le titre, le texte et les actions). Variez la distance et la durée selon le rôle plutôt que le délai.
- Limiter le décalage d'une liste à l'index 6 (un dernier délai de 240ms) ; les éléments au-delà partagent ce délai. L'entrée fonctionne pendant qu'il joue.
- Ne jamais décaler un menu, une palette de commandes, le contenu d'un onglet ou toute ouverture répétée.
- Restez immobile : tout ce que l'utilisateur s'apprête à cliquer, tout ce qui est déjà à l'écran lorsque la page se recharge, et une mise à jour qui arrive dans les 150ms suivant la précédente (sautez à l'état final).
- La direction suit le sens : le suivant entre par la droite, le retour par la gauche ; les choses que l'utilisateur a déclenchées s'agrandissent à partir du déclencheur.
Charger en cas de besoin
- references/tokens.md : chargez pour toute durée exacte, distance, échelle, flou, courbe d'assouplissement ou valeur de spring, et pour tokeniser une base de code existante.
- references/recipes.md : chargez lors de la construction d'un menu, popover, dialog, tiroir, toast, entrée de liste, échange d'icône, changement de hauteur, indicateur d'onglet ou changement de nombre.
- references/gestures.md : chargez pour le glissement, le swipe, le flick, les panneaux, ou tout ce qui suit un pointeur.
- references/performance.md : chargez lorsque l'animation perd des images, avant d'utiliser
will-change, et lorsque les mises à jour arrivent rapidement.
Paires
| Cassé | Corrigé | Pourquoi |
|---|---|---|
.menu { transition: all 300ms ease-in } | transition: opacity 200ms, transform 200ms var(--ease-out) | Propriétés nommées ; ease-in est lent ; menus ≤ 200ms |
scale(0) vers scale(1) | scale(0.95) et opacity: 0 vers 1 | Rien n'apparaît de rien |
| Popover se met à l'échelle depuis son centre | transform-origin: var(--transform-origin) | Il doit grandir à partir de son déclencheur |
.open { animation: slide-in 300ms } | transition: transform sur l'état basculé | Les keyframes sautent lorsqu'elles sont interrompues |
Chaque ligne animation-delay: i * 120ms | min(i, 6) * 40ms | La dernière ligne arrive en moins de 300ms |
| Le tiroir disparaît en fondu après être entré en glissant | Il glisse vers l'arrière par où il est venu | La direction est l'explication |
Relâchement du glissement sur un ease-out fixe | Un spring amorcé avec la vélocité de relâchement | L'élan de la main continue |
setY(y) dans pointermove | el.style.transform = ... | Pas de nouveau rendu par image |
Avant de terminer
Cherchez dans le diff, puis vérifiez.
| Rechercher | Correction |
|---|---|
transition: all, will-change sur *, ease-in | Propriétés nommées, supprimez, --ease-out |
scale(0), transform-origin: center sur un popover | 0.95, l'origine du déclencheur (les dialogues restent centrés) |
@keyframes sur une classe qui bascule | Une transition |
| Une durée au-dessus de sa ligne dans tokens.md, ou un nombre pas dans tokens.md | Le token de l'usage |
Animation sur les gestionnaires keydown ou une palette | Supprimez-la |
width, height, top, left, box-shadow dans une transition | transform, opacity, ou la recette |
Pas de chemin prefers-reduced-motion | Ajoutez-le |
Vérifiez dans l'application en cours d'exécution : jouez-la à 4× (panneau Animations des DevTools, ou multipliez les durées), spammez n'importe quel interrupteur, testez avec le mouvement réduit activé, et limitez le CPU à 4×. Dites lesquels de ces tests vous n'avez pas pu exécuter.