CSS vous donne deux outils pour le mouvement : les transitions et les animations @keyframes. La plupart des développeurs savent que les deux existent mais optent pour celui qu'ils ont appris en premier — souvent les transitions — puis se demandent pourquoi le spinner de chargement ne boucle pas ou pourquoi la notification revient en sursaut à sa position initiale. La décision n'est pas compliquée une fois que vous comprenez ce à quoi chacun est destiné. Les transitions sont une réaction : quelque chose change d'état, le navigateur lisse le changement. Les animations @keyframes sont des scripts autonomes : ils s'exécutent d'eux-mêmes, bouclent si vous le souhaitez, et n'ont pas besoin d'un déclencheur d'état. Ce guide couvre les deux, les propriétés qui comptent le plus, et les détails de performance qui séparent les animations fluides à 60fps des saccades. Si vous écrivez beaucoup de CSS, le Formateur CSS vaut la peine d'être mis en favoris — il nettoiera rapidement tout raccourci désordonné.
Transitions CSS — Le cas simple
Une transition dit au navigateur : "quand cette propriété change, anime le changement dans le temps plutôt que de sauter." C'est tout. L'utilisation la plus courante est les effets de survol — changements de couleur, fondus d'opacité, transformations d'échelle subtiles. Le guide des transitions MDN couvre la spécification complète, mais les quatre propriétés qui vous importent sont transition-property, transition-duration, transition-timing-function, et transition-delay.
/* The shorthand: property duration easing delay */
.btn-primary {
background-color: #4f46e5;
color: #fff;
padding: 0.625rem 1.25rem;
border-radius: 6px;
border: none;
cursor: pointer;
transition: background-color 200ms ease, transform 150ms ease;
}
.btn-primary:hover {
background-color: #4338ca;
transform: translateY(-1px);
}
.btn-primary:active {
transform: translateY(0);
}
/* Opacity fade — a card coming into view */
.notification-card {
opacity: 0;
transition: opacity 300ms ease-in-out;
}
.notification-card.is-visible {
opacity: 1;
}Vous pouvez lister plusieurs transitions séparées par des virgules, chacune avec sa propre durée et son propre timing — c'est ce que fait l'exemple de bouton ci-dessus. Chaque propriété s'anime indépendamment.
transition: all. C'est tentant comme solution universelle, mais cela signifie que chaque changement de propriété sur cet élément est animé — y compris ceux que vous n'aviez pas l'intention d'animer. Cela fait aussi travailler davantage le navigateur à chaque frame. Listez les propriétés spécifiques que vous souhaitez animer. La documentation MDN transition-property explique quelles propriétés sont animables.transition-timing-function — L'accélération expliquée
La fonction de timing contrôle la courbe d'accélération de l'animation — si elle démarre vite et ralentit, se déplace à une vitesse constante, ou rebondit selon une courbe personnalisée. Les mots-clés intégrés couvrent la plupart des situations.
ease— démarre lentement, accélère, puis ralentit vers la fin. La valeur par défaut. Bonne pour la plupart des animations UI.linear— vitesse constante tout au long. Bonne pour les spinners et barres de progression, mauvaise pour la plupart des autres choses — semble mécanique.ease-in— démarre lentement, finit vite. Donne l'impression de quelque chose qui prend de la vitesse. Bonne pour les éléments quittant l'écran.ease-out— démarre vite, finit lentement. Semble naturel pour les éléments entrant à l'écran (comme une notification qui glisse).ease-in-out— lent aux deux extrémités. Paraît soigné pour les éléments qui démarrent et s'arrêtent en vue.cubic-bezier(x1, y1, x2, y2)— définissez votre propre courbe. Des outils comme cubic-bezier.com vous permettent de créer et de prévisualiser des courbes personnalisées visuellement.
/* Custom spring-like easing with cubic-bezier */
.drawer {
transform: translateX(-100%);
transition: transform 350ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.drawer.is-open {
transform: translateX(0);
}
/* The cubic-bezier values above overshoot slightly (y > 1)
which gives a satisfying spring effect on open */@keyframes — Quand les transitions ne suffisent pas
Les transitions ont une limite difficile : elles nécessitent deux états et un déclencheur. Si vous voulez que quelque chose boucle indéfiniment, s'exécute au chargement de la page sans interaction, ou s'anime à travers plus de deux étapes, vous avez besoin de @keyframes. La référence MDN @keyframes couvre la syntaxe complète. Vous définissez les keyframes séparément, puis les attachez à un élément avec la propriété animation.
/* Define the animation */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Attach it to an element */
.spinner {
width: 24px;
height: 24px;
border: 3px solid #e5e7eb;
border-top-color: #4f46e5;
border-radius: 50%;
animation: spin 700ms linear infinite;
}
/* Multi-step — more than two stops */
@keyframes pulse-ring {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.15); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
.badge-live {
animation: pulse-ring 1.5s ease-in-out infinite;
}Le raccourci animation — Les huit propriétés
Le raccourci animation regroupe huit propriétés en une seule déclaration. Vous n'en avez pas besoin de toutes à chaque fois, mais savoir ce que chacune contrôle vous évite de fouiller dans la documentation quand quelque chose ne se comporte pas correctement.
/*
animation: name | duration | timing-function | delay | iteration-count | direction | fill-mode | play-state
*/
.toast {
animation: slide-in-right 350ms ease-out 0s 1 normal forwards running;
}
/* More commonly written as just the values you need: */
.toast {
animation: slide-in-right 350ms ease-out forwards;
}
/* Breaking it out to individual properties for clarity: */
.confetti-piece {
animation-name: float-down;
animation-duration: 2s;
animation-timing-function: ease-in;
animation-delay: calc(var(--i) * 150ms); /* staggered via CSS custom property */
animation-iteration-count: 1;
animation-direction: normal;
animation-fill-mode: forwards;
animation-play-state: running;
}
/* Pausing an animation via JavaScript: toggle a class */
.spinner.is-paused {
animation-play-state: paused;
}animation-fill-mode — Résoudre le problème du retour en sursaut
C'est la propriété qui pose problème à tout le monde à un moment donné. Par défaut, quand une animation se termine, l'élément revient en sursaut à ses styles pré-animation — comme si l'animation n'avait jamais eu lieu. Si vous animez une notification toast glissant depuis la droite, elle reviendra hors de l'écran en sursaut au moment où l'animation se terminera. animation-fill-mode: forwards résout cela en maintenant l'élément dans son état de keyframe final une fois l'animation terminée.
@keyframes slide-in-right {
from {
transform: translateX(110%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Without forwards: toast slides in, then SNAPS back off-screen */
.toast-bad {
animation: slide-in-right 350ms ease-out;
}
/* With forwards: toast slides in and STAYS in position */
.toast {
animation: slide-in-right 350ms ease-out forwards;
}
/*
fill-mode values:
- none (default) — no styles applied before or after
- forwards — hold the final keyframe after the animation ends
- backwards — apply the first keyframe during the delay period
- both — forwards + backwards combined
*/Exemples réels — Skeleton Loader, Spinner, Toast, Badge pulsant
Voici quatre modèles que vous utiliserez constamment. Chacun démontre une combinaison spécifique de @keyframes et de propriétés d'animation.
/* --- 1. Skeleton loading shimmer --- */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite linear;
border-radius: 4px;
}
.skeleton-title { height: 20px; width: 60%; margin-bottom: 12px; }
.skeleton-text { height: 14px; width: 100%; margin-bottom: 8px; }
.skeleton-text:last-child { width: 80%; }/* --- 2. Spinning loader --- */
@keyframes spin {
to { transform: rotate(360deg); }
}
.loader-spinner {
display: inline-block;
width: 32px;
height: 32px;
border: 3px solid rgba(79, 70, 229, 0.2);
border-top-color: #4f46e5;
border-radius: 50%;
animation: spin 600ms linear infinite;
}/* --- 3. Slide-in notification toast --- */
@keyframes toast-enter {
from {
transform: translateX(calc(100% + 1.5rem));
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes toast-exit {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(calc(100% + 1.5rem));
opacity: 0;
}
}
.toast {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
padding: 0.875rem 1.25rem;
background: #1f2937;
color: #fff;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
animation: toast-enter 350ms ease-out forwards;
}
.toast.is-dismissing {
animation: toast-exit 300ms ease-in forwards;
}/* --- 4. Pulsing notification badge --- */
@keyframes badge-pulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.5);
}
50% {
box-shadow: 0 0 0 6px rgba(239, 68, 68, 0);
}
}
.notification-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 5px;
background: #ef4444;
color: #fff;
font-size: 11px;
font-weight: 700;
border-radius: 999px;
animation: badge-pulse 2s ease-in-out infinite;
}Performance — Propriétés composites GPU vs déclenchant le layout
Toutes les propriétés CSS ne sont pas égales en matière de performance d'animation. Le pipeline de rendu du navigateur comporte trois étapes importantes ici : le layout (calcul des tailles et positions des éléments), la peinture (remplissage des pixels), et le composite (combinaison des couches sur le GPU). Animer une propriété qui déclenche le layout signifie que le navigateur recalcule la géométrie de tout le document à chaque frame — c'est coûteux, et c'est ce qui cause les saccades. Le guide de performance des animations de web.dev couvre cela en profondeur.
- Sûr à animer (composite GPU) :
transform(translate, scale, rotate) etopacity. Ceux-ci s'exécutent entièrement sur le thread composite GPU — ils ne déclenchent jamais le layout ou la peinture. - Cause de la peinture (à éviter si possible) :
color,background-color,border-color,box-shadow. Ceux-ci ignorent le layout mais déclenchent un repeint à chaque frame. Les transitions courtes (moins de 300ms) sont généralement acceptables. - Cause du layout (ne jamais animer) :
width,height,top,left,margin,padding. Chaque frame déclenche un recalcul complet du layout — saccades garanties sur des pages complexes.
transform et opacity. Si vous voulez déplacer quelque chose, utilisez translate et non left/top. Si vous voulez redimensionner quelque chose, utilisez scale et non width/height. CSS Triggers est une référence qui liste exactement quelles étapes de rendu chaque propriété déclenche./* Bad — animating width triggers layout on every frame */
.progress-bar-bad {
transition: width 300ms ease;
}
/* Good — use scaleX transform instead */
.progress-bar {
transform-origin: left center;
transition: transform 300ms ease;
}
/* In JS, set: progressBar.style.transform = 'scaleX(0.75)' for 75% */
/* Bad — animating top/left (positions element with layout) */
.tooltip-bad {
position: absolute;
top: 0;
transition: top 200ms ease;
}
/* Good — use translateY instead */
.tooltip {
position: absolute;
top: 0;
transform: translateY(0);
transition: transform 200ms ease;
}will-change — Utilisez-le avec parcimonie
will-change est un indice au navigateur qu'une propriété spécifique est sur le point d'être animée, il devrait donc promouvoir l'élément dans sa propre couche composite à l'avance. Cela peut éliminer le bref flash de saccade que vous voyez parfois au tout début d'une animation sur du matériel bas de gamme. Mais cela a un vrai coût : chaque couche promue consomme de la mémoire GPU. Si vous mettez will-change: transform sur chaque élément animé de votre application, vous dégraderez les performances sur la plupart des appareils — l'inverse de ce que vous vouliez.
/* Right way — add it just before animation starts, remove after */
.modal-overlay {
/* Don't set will-change here by default */
}
/* Add via JavaScript only when user triggers modal open */
/* overlay.style.willChange = 'opacity'; */
/* overlay.addEventListener('transitionend', () => { */
/* overlay.style.willChange = 'auto'; */
/* }); */
/* Acceptable in CSS for elements that animate frequently
(e.g., a persistent floating action button on scroll) */
.fab {
will-change: transform; /* OK — this element genuinely animates on scroll */
transition: transform 200ms ease;
}
/* Don't do this — wastes GPU memory with zero benefit */
.card {
will-change: transform; /* BAD — card only animates on hover, not constantly */
}prefers-reduced-motion — L'accessibilité que vous ne pouvez pas ignorer
Une part significative des utilisateurs souffre de troubles vestibulaires ou d'autres conditions où le mouvement déclenche un inconfort ou des nausées. La media query prefers-reduced-motion vous permet de respecter le paramètre système "réduire le mouvement". La directive WCAG 2.1 2.3.3 couvre cette exigence, et la référence MDN prefers-reduced-motion montre la compatibilité navigateur (elle est universelle maintenant). Le modèle est simple : enveloppez vos animations dans la media query standard, et fournissez une alternative sans mouvement dans celle de mouvement réduit.
/* Define animations normally for users who are fine with motion */
@keyframes slide-in-right {
from { transform: translateX(110%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.toast {
animation: slide-in-right 350ms ease-out forwards;
}
.spinner {
animation: spin 600ms linear infinite;
}
/* Override for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
.toast {
/* No sliding — just appear */
animation: none;
opacity: 1;
transform: none;
}
.spinner {
/* Slow it way down or stop it entirely */
animation-duration: 4s;
}
/* Nuclear option — disable ALL animations site-wide */
/* Use this only if you haven't audited each animation individually */
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}L'"option nucléaire" à la fin de ce snippet est un modèle courant pour les bases de code existantes qui n'ont pas audité chaque animation. C'est mieux que rien, mais auditer chaque animation individuellement vous donne plus de contrôle — certaines animations communiquent un état (une barre de progression, un spinner de chargement) et devraient être conservées, juste ralenties.
Transitions vs @keyframes — Le guide de décision
Si vous ne savez jamais lequel choisir, posez-vous deux questions sur l'animation que vous construisez :
- Est-elle déclenchée par un changement d'état ? (survol, focus, bascule de classe, case à cocher) → Utilisez une transition. C'est exactement pour cela que les transitions sont conçues.
- Boucle-t-elle indéfiniment ? → Utilisez @keyframes avec
animation-iteration-count: infinite. - A-t-elle plus de deux étapes ? (pas seulement début → fin, mais début → milieu → fin ou plus) → Utilisez @keyframes.
- Doit-elle s'exécuter au chargement de la page, sans interaction de l'utilisateur ? → Utilisez @keyframes.
- L'élément doit-il rester dans son état final après la fin ? → Utilisez @keyframes avec
animation-fill-mode: forwards.
Pour conclure
Les transitions gèrent le mouvement piloté par l'état. @keyframes gère tout le reste. Animez toujours transform et opacity pour la performance — laissez width, height, top, et left de côté. Utilisez animation-fill-mode: forwards chaque fois que vous avez besoin que l'élément conserve son état final. Ajoutez l'override prefers-reduced-motion avant de livrer quoi que ce soit — c'est un ajout de dix lignes qui compte beaucoup pour de vrais utilisateurs. Pour en savoir plus sur le modèle de composition et ce qui déclenche le layout, les docs de performance de rendu de web.dev sont la ressource la plus pratique disponible. Une fois le CSS écrit, faites-le passer par le Formateur CSS pour garder le raccourci lisible, ou par le Minificateur CSS pour supprimer les espaces avant de livrer en production.