Aller au contenu
Bibliotheque UIJABS

Animations 27 variantes

L'animation d'interface, par opposition au mouvement au defilement. Ici tout part d'une ACTION : un clic, une saisie, l'arrivee d'une reponse. Cinq regles tenues partout : on n'anime que transform et opacity (animer une largeur force un recalcul de mise en page a chaque image), une transition dure 120 a 320 ms, la courbe compte plus que la duree, l'entree est plus lente que la sortie, et prefers-reduced-motion supprime le mouvement sans jamais supprimer l'information. Passez la souris sur une demonstration pour la rejouer.

A. Micro-interactions

Icone qui se transforme, coche qui se trace, case, favori, champ au focus, bouton d'envoi. Le retour le plus frequent d'une interface.

Icone qui se transforme

Le menu qui devient croix, la lecture qui devient pause. Les deux etats sont le MEME objet : c'est ce qui fait comprendre que le bouton refermera ce qu'il vient d'ouvrir.

Voir le codeMasquer le code
<div class="demo-row">
  <button class="btn btn--icone btn--secondaire an-burger" aria-label="Ouvrir le menu" aria-expanded="false"><i></i></button>
  <button class="btn btn--icone btn--secondaire an-burger" aria-label="Fermer le menu" aria-expanded="true"><i></i></button>
  <button class="btn btn--icone btn--secondaire an-lecture" aria-label="Lecture" aria-pressed="false"><i></i></button>
  <button class="btn btn--icone btn--secondaire an-lecture" aria-label="Pause" aria-pressed="true"><i></i></button>
</div>
.an-burger i, .an-burger i::before, .an-burger i::after {
  display: block;
  width: 17px; height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: transform 260ms var(--e-douce), opacity 160ms var(--e-douce);
}
.an-burger i { position: relative; }
.an-burger i::before, .an-burger i::after { content: ""; position: absolute; left: 0; }
.an-burger i::before { top: -6px; }
.an-burger i::after  { top: 6px; }
.an-burger[aria-expanded="true"] i { transform: rotate(45deg); }
.an-burger[aria-expanded="true"] i::before { transform: translateY(6px) rotate(-90deg); }
.an-burger[aria-expanded="true"] i::after  { opacity: 0; transform: scaleX(0); }
/* Lecture / pause : deux barres qui se rejoignent en triangle.
   Chacune porte un clip-path : accolees et decoupees en biseau elles
   forment le triangle, ecartees et pleines elles forment la pause. */
.an-lecture i { position: relative; display: block; width: 14px; height: 15px; }
.an-lecture i::before, .an-lecture i::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0;
  width: 5px;
  background: currentColor;
  border-radius: 1px;
  transition: clip-path 280ms var(--e-douce), transform 280ms var(--e-douce), border-radius 280ms var(--e-douce);
}
.an-lecture i::before { left: 0; }
.an-lecture i::after  { left: 9px; }
.an-lecture[aria-pressed="false"] i::before {
  clip-path: polygon(0 0, 100% 22%, 100% 78%, 0 100%);
  border-radius: 1px 0 0 1px;
}
.an-lecture[aria-pressed="false"] i::after {
  clip-path: polygon(0 22%, 100% 50%, 100% 50%, 0 78%);
  transform: translateX(-4px);
  border-radius: 0;
}
les trois barres pivotent et se rejoignent ; aucune icone n'est remplacee
Coche qui se trace

La confirmation d'une action reussie. Le trait se dessine au lieu d'apparaitre : le regard suit le geste et comprend que quelque chose vient de se terminer.

Voir le codeMasquer le code
<div class="demo-row">
  <span class="an-coche" data-an-rejouer><svg viewBox="0 0 42 42" aria-hidden="true"><circle cx="21" cy="21" r="19"/><path d="M12 21.5 18.5 28 30 15.5"/></svg></span>
  <span class="an-coche an-coche--ko" data-an-rejouer><svg viewBox="0 0 42 42" aria-hidden="true"><circle cx="21" cy="21" r="19"/><path d="M14 14 28 28M28 14 14 28"/></svg></span>
</div>
.an-coche svg { width: 54px; height: 54px; overflow: visible; }
.an-coche circle, .an-coche path {
  fill: none;
  stroke: var(--c-success);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* La longueur exacte du trace sert de dasharray : le trait se dessine. */
.an-coche circle { stroke-dasharray: 120; stroke-dashoffset: 120; animation: an-tracer 620ms var(--e-douce) forwards; }
.an-coche path   { stroke-dasharray: 40;  stroke-dashoffset: 40;  animation: an-tracer 320ms var(--e-douce) 380ms forwards; }
@keyframes an-tracer { to { stroke-dashoffset: 0; } }
.an-coche--ko circle, .an-coche--ko path { stroke: var(--c-danger); }
stroke-dasharray + stroke-dashoffset animes de la longueur du trace a zero
Case et interrupteur

Le retour le plus frequent d'une interface. La coche se trace, la poignee s'etire pendant le glissement : deux details qui rendent le controle tangible.

Voir le codeMasquer le code
<div class="demo-col">
  <label class="coche an-coche-anim"><input type="checkbox"><span class="coche__boite"></span> Recevoir les alertes</label>
  <label class="coche an-coche-anim"><input type="checkbox" checked><span class="coche__boite"></span> Relances automatiques</label>
  <label class="bascule"><input type="checkbox" checked><span class="bascule__piste"></span> Signature obligatoire</label>
</div>
.an-coche-anim .coche__boite { transition: background-color 160ms var(--e-douce), border-color 160ms var(--e-douce), transform 200ms var(--e-ressort); }
.an-coche-anim:active .coche__boite { transform: scale(.86); }
.an-coche-anim .coche__boite::after { transition: transform 260ms var(--e-ressort) 40ms; }
la boite passe en scale(.85) au clic puis revient : le controle repond au doigt
Bouton favori

Le geste le plus copie du web, et pour une raison : le depassement d'echelle donne une sensation de declic que la couleur seule ne produit pas.

Voir le codeMasquer le code
<div class="demo-row">
  <button class="btn btn--icone btn--secondaire an-coeur" aria-pressed="false" aria-label="Ajouter aux favoris"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1Z"/></svg></button>
  <button class="btn btn--icone btn--secondaire an-coeur" aria-pressed="true" aria-label="Retirer des favoris"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1Z"/></svg></button>
  <button class="btn btn--secondaire an-coeur" aria-pressed="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1Z"/></svg> 24</button>
</div>
.an-coeur svg { transition: transform 340ms var(--e-ressort), fill 200ms var(--e-douce); }
.an-coeur[aria-pressed="true"] { border-color: var(--c-warn); color: var(--c-warn); background: var(--c-warn-soft); }
.an-coeur[aria-pressed="true"] svg { fill: currentColor; animation: an-declic 420ms var(--e-ressort); }
@keyframes an-declic {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.38); }
  100% { transform: scale(1); }
}
scale(1.35) puis retour en cubic-bezier(.34,1.56,.64,1)
Champ qui prend le focus

L'anneau grandit depuis le bord au lieu d'apparaitre d'un coup, et l'intitule remonte. Deux details qui font percevoir le champ comme actif plutot que simplement surligne.

Voir le codeMasquer le code
<div class="demo-col demo-col--large">
  <label class="an-champ"><input class="saisie" placeholder=" "><span>Nom du client</span></label>
  <label class="an-champ"><input class="saisie" value="Krissi Julien" placeholder=" "><span>Nom du client</span></label>
</div>
.an-champ { position: relative; display: block; }
.an-champ .saisie {
  padding-top: 16px;
  transition: box-shadow 180ms var(--e-douce), border-color 140ms var(--e-douce);
  height: 52px;
}
.an-champ span {
  position: absolute;
  left: 13px; top: 16px;
  font-size: var(--fs-md);
  color: var(--c-text-muted);
  pointer-events: none;
  transform-origin: 0 0;
  transition: transform 200ms var(--e-douce), color 200ms var(--e-douce);
}
.an-champ .saisie:focus + span,
.an-champ .saisie:not(:placeholder-shown) + span { transform: translateY(-9px) scale(.78); }
.an-champ .saisie:focus + span { color: var(--c-primary); }
label en translate + scale, anneau en box-shadow anime depuis 0
Bouton d'envoi

Le parcours complet d'une action : repos, envoi, succes, retour au repos. La largeur ne bouge pas d'un pixel, sinon la mise en page saute a chaque etape.

Voir le codeMasquer le code
<div class="demo-row">
  <button class="btn btn--plein an-envoi">Envoyer le devis</button>
  <button class="btn btn--plein an-envoi is-encours"><span class="btn__spin"></span>Envoi</button>
  <button class="btn btn--plein an-envoi is-fait"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20 6-11 11-5-5"/></svg> Envoye</button>
</div>
.an-envoi { min-width: 160px; transition: background-color 260ms var(--e-douce), transform 140ms var(--e-douce); }
.an-envoi.is-encours { pointer-events: none; opacity: .9; }
.an-envoi.is-fait { background: var(--c-success); pointer-events: none; }
.an-envoi.is-fait svg { animation: an-declic 420ms var(--e-ressort); }
min-width figee avant le changement d'etat, transition sur background-color

B. Transitions d'etat

Accordeon fluide, onglet glissant, entree de modale, reordonnancement FLIP, suppression en deux temps, bandeau qui pousse le contenu.

Quelles aides puis-je obtenir ?

MaPrimeRenov, les CEE et la TVA a 5,5 % selon vos revenus et le logement. Nous montons le dossier et deduisons les aides du devis.

Combien de temps dure la pose ?

Une journee pour un chauffe-eau, deux a trois jours pour une pompe a chaleur avec depose de l'ancienne chaudiere.

Intervenez-vous en urgence ?

Oui, 7 jours sur 7, avec un delai moyen de 2 heures en Ile-de-France.

Accordeon fluide

Le repliement le plus demande et le plus souvent rate. `interpolate-size` rend `height: auto` animable : plus besoin de mesurer la hauteur en JavaScript.

Voir le codeMasquer le code
<div class="an-accordeon">
  <details open><summary>Quelles aides puis-je obtenir ?<i></i></summary><div><p>MaPrimeRenov, les CEE et la TVA a 5,5 % selon vos revenus et le logement. Nous montons le dossier et deduisons les aides du devis.</p></div></details>
  <details><summary>Combien de temps dure la pose ?<i></i></summary><div><p>Une journee pour un chauffe-eau, deux a trois jours pour une pompe a chaleur avec depose de l'ancienne chaudiere.</p></div></details>
  <details><summary>Intervenez-vous en urgence ?<i></i></summary><div><p>Oui, 7 jours sur 7, avec un delai moyen de 2 heures en Ile-de-France.</p></div></details>
</div>
/* Debloque l'animation vers `auto`. Sans cette ligne, la hauteur saute. */
.an-accordeon { interpolate-size: allow-keywords; }
.an-accordeon details { border-bottom: 1px solid var(--c-border); }
.an-accordeon summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--sp-4) 0;
  font-size: var(--fs-lg);
  font-weight: var(--fw-semi);
  cursor: pointer;
  list-style: none;
}
.an-accordeon summary::-webkit-details-marker { display: none; }
.an-accordeon summary i {
  position: relative;
  width: 14px; height: 14px;
  flex: none;
}
.an-accordeon summary i::before, .an-accordeon summary i::after {
  content: "";
  position: absolute;
  inset: 6px 0;
  background: var(--c-text-muted);
  border-radius: 1px;
  transition: transform 280ms var(--e-douce);
}
.an-accordeon summary i::after { transform: rotate(90deg); }
.an-accordeon details[open] summary i::after { transform: rotate(0); }
.an-accordeon details::details-content {
  block-size: 0;
  overflow: hidden;
  opacity: 0;
  transition: block-size 320ms var(--e-douce), opacity 240ms var(--e-douce), content-visibility 320ms allow-discrete;
}
.an-accordeon details[open]::details-content { block-size: auto; opacity: 1; }
.an-accordeon p { margin: 0 0 var(--sp-4); font-size: var(--fs-md); line-height: 1.65; color: var(--c-text-soft); max-width: 68ch; }
/* Repli pour les navigateurs sans ::details-content : le contenu apparait
   sans animation, ce qui reste parfaitement utilisable. */
@supports not selector(details::details-content) {
  .an-accordeon details > div { animation: an-deplier 300ms var(--e-douce); }
  @keyframes an-deplier { from { opacity: 0; transform: translateY(-6px); } }
}
interpolate-size: allow-keywords + transition-behavior: allow-discrete
Indicateur d'onglet glissant

Le trait glisse d'un onglet a l'autre au lieu de sauter. Il montre le chemin parcouru, ce qui aide a comprendre ou l'on va dans une serie d'etapes.

Voir le codeMasquer le code
<div class="an-onglets" style="--i:1;--n:4" data-an-onglets>
  <button aria-selected="false">Resume</button>
  <button aria-selected="true">Documents</button>
  <button aria-selected="false">Interventions</button>
  <button aria-selected="false">Facturation</button>
  <span class="an-onglets__trait"></span>
</div>
.an-onglets { position: relative; display: flex; border-bottom: 1px solid var(--c-border); }
.an-onglets button {
  flex: 1;
  padding: var(--sp-3);
  border: none;
  background: transparent;
  font-family: var(--font-ui);
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  color: var(--c-text-muted);
  cursor: pointer;
  transition: color 200ms var(--e-douce);
}
.an-onglets button[aria-selected="true"] { color: var(--c-primary-ink); font-weight: var(--fw-semi); }
.an-onglets__trait {
  position: absolute;
  bottom: -1px; left: 0;
  height: 2px;
  width: calc(100% / var(--n));
  border-radius: 2px 2px 0 0;
  background: var(--c-primary);
  transform: translateX(calc(var(--i) * 100%));
  transition: transform 320ms var(--e-douce);
}
translateX(calc(var(--i) * 100%)) sur un trait de largeur 1/n

Envoyer le devis

Le voile se fond, la fenetre monte de 12 px en grandissant depuis 0,96.

Entree d'une modale

`@starting-style` permet d'animer l'apparition d'un element qui vient d'etre insere, sans classe ajoutee en JavaScript ni double `requestAnimationFrame`.

Voir le codeMasquer le code
<div class="modale-scene an-modale-scene">
  <div class="modale__voile"></div>
  <div class="modale an-modale">
    <div class="modale__tete"><h3>Envoyer le devis</h3><button class="btn btn--fermer" aria-label="Fermer"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button></div>
    <div class="modale__corps"><p>Le voile se fond, la fenetre monte de 12 px en grandissant depuis 0,96.</p></div>
    <div class="modale__pied"><button class="btn btn--secondaire">Annuler</button><button class="btn btn--plein">Envoyer</button></div>
  </div>
</div>
.an-modale {
  animation: an-modale-entree 280ms var(--e-douce) both;
}
@keyframes an-modale-entree {
  from { opacity: 0; transform: translateY(12px) scale(.96); }
}
.an-modale-scene .modale__voile { animation: an-fondu 240ms var(--e-douce) both; }
@keyframes an-fondu { from { opacity: 0; } }
/* Sur un vrai <dialog> ou un popover, @starting-style evite toute classe JS :
   dialog[open] { opacity: 1 } @starting-style { dialog[open] { opacity: 0 } } */
@starting-style { opacity: 0; transform: translateY(12px) scale(.96) }
Reordonnancement de liste

La technique FLIP : on mesure les positions avant et apres, on applique la difference a rebours, puis on la laisse revenir a zero. C'est ce qui permet d'animer un changement de mise en page sans animer la mise en page.

Voir le codeMasquer le code
<div class="an-flip" data-an-flip>
  <div class="demo-row" style="margin-bottom:12px">
    <button class="btn btn--secondaire btn--sm" data-an-melanger>Reordonner</button>
    <span class="champ__aide">Les lignes glissent vers leur nouvelle place.</span>
  </div>
  <ul class="liste">
    <li data-cle="a"><a href="#"><span class="personne__avatar">JK</span><span class="liste__bloc"><strong>Krissi Julien</strong><small>Neuilly-sur-Marne</small></span><strong>4 280 EUR</strong></a></li>
    <li data-cle="b"><a href="#"><span class="personne__avatar personne__avatar--vert">MS</span><span class="liste__bloc"><strong>Martin Sophie</strong><small>Colombes</small></span><strong>1 890 EUR</strong></a></li>
    <li data-cle="c"><a href="#"><span class="personne__avatar">DP</span><span class="liste__bloc"><strong>Durand Paul</strong><small>Le Raincy</small></span><strong>12 400 EUR</strong></a></li>
    <li data-cle="d"><a href="#"><span class="personne__avatar personne__avatar--vert">BA</span><span class="liste__bloc"><strong>Bernard Alice</strong><small>Gagny</small></span><strong>6 750 EUR</strong></a></li>
  </ul>
</div>
.an-flip li { transition: transform 420ms var(--e-douce); }
JS : First, Last, Invert, Play (voir js/anim.js)
Suppression d'une ligne

La ligne se retire vers la droite puis sa hauteur se referme. En deux temps : sans cela, les lignes du dessous sautent avant que l'oeil ait compris ce qui partait.

Voir le codeMasquer le code
<div class="an-suppr" data-an-suppr>
  <ul class="liste">
    <li><a href="#"><span class="liste__bloc"><strong>Devis 2026-103</strong><small>Signe le 12 aout</small></span><button class="btn btn--icone btn--fantome btn--sm" data-an-retirer aria-label="Retirer"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button></a></li>
    <li><a href="#"><span class="liste__bloc"><strong>Devis 2026-118</strong><small>Envoye le 29 aout</small></span><button class="btn btn--icone btn--fantome btn--sm" data-an-retirer aria-label="Retirer"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button></a></li>
    <li><a href="#"><span class="liste__bloc"><strong>Devis 2026-121</strong><small>Brouillon</small></span><button class="btn btn--icone btn--fantome btn--sm" data-an-retirer aria-label="Retirer"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button></a></li>
  </ul>
  <button class="btn btn--secondaire btn--sm" data-an-restaurer style="margin-top:12px">Restaurer</button>
</div>
.an-suppr li { display: grid; grid-template-rows: 1fr; transition: grid-template-rows 280ms var(--e-douce) 180ms; }
.an-suppr li > * { overflow: hidden; }
.an-suppr li.part > a { animation: an-partir 200ms var(--e-sortie) both; }
.an-suppr li.part { grid-template-rows: 0fr; }
@keyframes an-partir { to { opacity: 0; transform: translateX(34px); } }
etape 1 : translateX + opacity ; etape 2 : grid-template-rows 1fr vers 0fr
3 devis attendent une relance

Envoyes il y a plus de 7 jours.

Le contenu suivant est pousse vers le bas, sans saut.

Bandeau qui s'ouvre

Une alerte inseree en haut de page. Elle pousse le contenu au lieu de le recouvrir, et la poussee est animee : personne ne perd sa ligne de lecture.

Voir le codeMasquer le code
<div class="an-bandeau" data-an-bandeau>
  <button class="btn btn--secondaire btn--sm" data-an-basculer>Afficher ou masquer</button>
  <div class="an-bandeau__zone">
    <div class="alerte alerte--attention"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 9v4M12 17h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/></svg></span><div><strong>3 devis attendent une relance</strong><p>Envoyes il y a plus de 7 jours.</p></div></div>
  </div>
  <p class="mv-note">Le contenu suivant est pousse vers le bas, sans saut.</p>
</div>
.an-bandeau__zone {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows 320ms var(--e-douce), margin 320ms var(--e-douce);
  margin: var(--sp-3) 0;
}
.an-bandeau__zone > * { overflow: hidden; }
.an-bandeau.is-ferme .an-bandeau__zone { grid-template-rows: 0fr; margin: 0; }
grid-template-rows: 0fr vers 1fr, la seule facon d'animer une hauteur inconnue

C. Traces et formes

Trace de ligne SVG, jauge circulaire, barres qui montent, forme organique. Tout par stroke-dashoffset et transform, jamais par la geometrie.

Trace de ligne

Un logo, une signature ou un pictogramme qui se dessine. Le trace suit l'ordre des chemins dans le SVG : c'est lui qui donne l'impression d'une main qui ecrit.

Voir le codeMasquer le code
<div class="demo-row" data-an-rejouer>
  <svg class="an-trace" viewBox="0 0 120 60" aria-hidden="true">
    <path d="M8 44 L8 16 L26 44 L26 16"/><path d="M40 16 L40 44 L58 44"/>
    <path d="M72 16 L72 44"/><path d="M86 16 Q104 16 104 30 Q104 44 86 44 L86 16"/>
  </svg>
  <svg class="an-trace an-trace--signature" viewBox="0 0 140 50" aria-hidden="true">
    <path d="M8 36 Q22 6 32 30 Q40 48 50 20 Q58 -2 66 32 Q72 46 84 26 Q96 8 104 30 Q110 44 132 18"/>
  </svg>
</div>
.an-trace { height: 62px; overflow: visible; }
.an-trace path {
  fill: none;
  stroke: var(--c-primary);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
  /* pathLength normalise la longueur a 1 : plus besoin de mesurer le trace. */
  pathLength: 1;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: an-tracer1 1.5s var(--e-douce) forwards;
}
.an-trace path:nth-child(2) { animation-delay: .3s; }
.an-trace path:nth-child(3) { animation-delay: .6s; }
.an-trace path:nth-child(4) { animation-delay: .8s; }
@keyframes an-tracer1 { to { stroke-dashoffset: 0; } }
.an-trace--signature path { stroke: var(--c-text); stroke-width: 2.4; animation-duration: 2.2s; }
stroke-dasharray: 1 ; stroke-dashoffset anime avec pathLength="1"
74 % 100 % 28 %
Jauge circulaire

Un taux qui se remplit a l'arrivee. La valeur chiffree est ecrite dans le balisage : elle reste juste si l'animation ne joue pas.

Voir le codeMasquer le code
<div class="demo-row">
  <span class="an-jauge" style="--p:.74"><svg viewBox="0 0 44 44" aria-hidden="true"><circle cx="22" cy="22" r="19"/><circle cx="22" cy="22" r="19" class="v"/></svg><em>74 %</em></span>
  <span class="an-jauge an-jauge--ok" style="--p:1"><svg viewBox="0 0 44 44" aria-hidden="true"><circle cx="22" cy="22" r="19"/><circle cx="22" cy="22" r="19" class="v"/></svg><em>100 %</em></span>
  <span class="an-jauge an-jauge--bas" style="--p:.28"><svg viewBox="0 0 44 44" aria-hidden="true"><circle cx="22" cy="22" r="19"/><circle cx="22" cy="22" r="19" class="v"/></svg><em>28 %</em></span>
</div>
.an-jauge { position: relative; display: inline-grid; place-items: center; width: 84px; height: 84px; }
.an-jauge svg { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }
.an-jauge circle { fill: none; stroke-width: 4; stroke: var(--c-surface-3); }
.an-jauge .v {
  stroke: var(--c-primary);
  stroke-linecap: round;
  pathLength: 1;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: an-remplir 1.1s var(--e-douce) forwards;
}
@keyframes an-remplir { to { stroke-dashoffset: calc(1 - var(--p)); } }
.an-jauge em { font-style: normal; font-size: var(--fs-md); font-weight: var(--fw-bold); font-variant-numeric: tabular-nums; }
.an-jauge--ok .v  { stroke: var(--c-success); }
.an-jauge--bas .v { stroke: var(--c-danger); }
pathLength="1" + stroke-dashoffset: calc(1 - var(--p))
avr
mai
juin
juil
aout
sept
Barres qui montent

Un histogramme qui se construit a l'arrivee dans le champ. Les barres partent de zero et se decalent legerement : le decalage suffit a donner un rythme.

Voir le codeMasquer le code
<div class="an-barres" data-an-rejouer>
  <div style="--h:42%;--i:0"><i></i><small>avr</small></div>
  <div style="--h:58%;--i:1"><i></i><small>mai</small></div>
  <div style="--h:51%;--i:2"><i></i><small>juin</small></div>
  <div style="--h:74%;--i:3"><i></i><small>juil</small></div>
  <div style="--h:63%;--i:4"><i></i><small>aout</small></div>
  <div style="--h:88%;--i:5" class="c"><i></i><small>sept</small></div>
</div>
.an-barres { display: flex; align-items: flex-end; gap: var(--sp-3); height: 150px; width: 100%; }
.an-barres > div { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; gap: 6px; height: 100%; }
.an-barres i {
  display: block;
  width: 100%;
  height: var(--h);
  border-radius: var(--r-sm) var(--r-sm) 0 0;
  background: var(--c-primary-soft);
  transform-origin: 50% 100%;
  /* scaleY plutot que height : aucun recalcul de mise en page. */
  animation: an-monter 700ms var(--e-douce) both;
  animation-delay: calc(var(--i) * 70ms);
}
.an-barres .c i { background: var(--c-primary); }
@keyframes an-monter { from { transform: scaleY(0); } }
.an-barres small { font-size: var(--fs-xs); color: var(--c-text-muted); }
transform-origin: bottom + scaleY(0) vers scaleY(1), decale de 60 ms par barre
Forme qui se deforme

Un fond organique qui respire. Les valeurs de `border-radius` sont interpolees entre trois etats : le mouvement parait continu sans jamais boucler visiblement.

Voir le codeMasquer le code
<div class="demo-row">
  <span class="an-blob"></span>
  <span class="an-blob an-blob--2"></span>
  <span class="an-blob an-blob--3"></span>
</div>
.an-blob {
  width: 110px; height: 110px;
  background: linear-gradient(140deg, #d8853f, #a8543c);
  animation: an-morph 11s ease-in-out infinite;
}
.an-blob--2 { background: linear-gradient(140deg, #3f7ea8, #3a4a8a); animation-duration: 13s; animation-direction: reverse; }
.an-blob--3 { background: linear-gradient(140deg, #3f9e7a, #2f6b5a); animation-duration: 17s; }
@keyframes an-morph {
  0%, 100% { border-radius: 62% 38% 46% 54% / 54% 46% 54% 46%; }
  33%      { border-radius: 38% 62% 63% 37% / 41% 44% 56% 59%; }
  66%      { border-radius: 54% 46% 38% 62% / 62% 54% 46% 38%; }
}
huit valeurs de border-radius animees en boucle, durees premieres entre elles

D. Attentes

Six indicateurs de chargement et le choix entre eux, squelette calque sur le contenu, progression par etapes.

Tourniquet — moins de 2 s
Points — jusqu'a 5 s
Barre indeterminee — au-dela
Pulsation — attente passive
Onde — traitement par lots
Squelette — forme connue
Six indicateurs de chargement

Choisir selon la duree : un tourniquet sous 2 secondes, des points jusqu'a 5, une barre au-dela, et un squelette quand la forme du contenu est connue d'avance.

Voir le codeMasquer le code
<div class="an-chargements">
  <div><span class="btn__spin an-spin"></span><small>Tourniquet &mdash; moins de 2 s</small></div>
  <div><span class="an-points"><i></i><i></i><i></i></span><small>Points &mdash; jusqu'a 5 s</small></div>
  <div><span class="an-barre"><i></i></span><small>Barre indeterminee &mdash; au-dela</small></div>
  <div><span class="an-pulse"></span><small>Pulsation &mdash; attente passive</small></div>
  <div><span class="an-onde-c"><i></i><i></i><i></i><i></i></span><small>Onde &mdash; traitement par lots</small></div>
  <div><span class="an-squelette"></span><small>Squelette &mdash; forme connue</small></div>
</div>
.an-chargements { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: var(--sp-5); }
.an-chargements > div { display: flex; flex-direction: column; align-items: center; gap: var(--sp-3); text-align: center; }
.an-chargements small { font-size: var(--fs-xs); color: var(--c-text-muted); line-height: 1.4; }
.an-spin { width: 26px; height: 26px; border-width: 2.5px; border-color: var(--c-surface-3); border-top-color: var(--c-primary); }
.an-points { display: inline-flex; gap: 6px; height: 26px; align-items: center; }
.an-points i { width: 8px; height: 8px; border-radius: 50%; background: var(--c-primary); animation: an-rebond 1.2s var(--e-douce) infinite; }
.an-points i:nth-child(2) { animation-delay: .15s; }
.an-points i:nth-child(3) { animation-delay: .3s; }
@keyframes an-rebond { 0%, 60%, 100% { transform: translateY(0); opacity: .45; } 30% { transform: translateY(-8px); opacity: 1; } }
.an-barre { display: block; width: 100%; height: 5px; border-radius: var(--r-full); background: var(--c-surface-3); overflow: hidden; }
.an-barre i { display: block; width: 38%; height: 100%; border-radius: inherit; background: var(--c-primary); animation: an-va-vient 1.4s var(--e-douce) infinite; }
@keyframes an-va-vient { 0% { transform: translateX(-105%); } 100% { transform: translateX(300%); } }
.an-pulse { width: 26px; height: 26px; border-radius: 50%; background: var(--c-primary); animation: an-respire 1.6s var(--e-douce) infinite; }
@keyframes an-respire { 0%, 100% { transform: scale(.8); opacity: .45; } 50% { transform: scale(1); opacity: 1; } }
.an-onde-c { display: inline-flex; gap: 4px; height: 26px; align-items: center; }
.an-onde-c i { width: 4px; height: 22px; border-radius: 2px; background: var(--c-primary); transform-origin: 50% 50%; animation: an-onde-b 1s var(--e-douce) infinite; }
.an-onde-c i:nth-child(2) { animation-delay: .1s; }
.an-onde-c i:nth-child(3) { animation-delay: .2s; }
.an-onde-c i:nth-child(4) { animation-delay: .3s; }
@keyframes an-onde-b { 0%, 100% { transform: scaleY(.35); } 50% { transform: scaleY(1); } }
.an-squelette {
  display: block; width: 100%; height: 26px; border-radius: var(--r-sm);
  background: linear-gradient(90deg, var(--c-surface-2) 25%, var(--c-surface-3) 50%, var(--c-surface-2) 75%);
  background-size: 200% 100%;
  animation: an-scintille 1.4s ease-in-out infinite;
}
@keyframes an-scintille { to { background-position: -200% 0; } }
six formes, une intention chacune ; jamais deux dans le meme ecran
Squelette calque sur le contenu

Le squelette prend la forme exacte de ce qui arrive : avatar rond, deux lignes, une valeur a droite. Toute autre forme fait sauter la page a l'arrivee des donnees.

Voir le codeMasquer le code
<div class="an-bascule-charge" data-an-charger>
  <button class="btn btn--secondaire btn--sm" data-an-recharger style="margin-bottom:12px">Recharger</button>
  <ul class="liste">
    <li><div class="squel-ligne"><span class="an-squelette squel-rond"></span><span class="squel-bloc"><span class="an-squelette" style="height:13px;width:45%;border-radius:3px"></span><span class="an-squelette" style="height:10px;width:65%;border-radius:3px"></span></span><span class="an-squelette" style="height:13px;width:70px;border-radius:3px"></span></div></li>
    <li><div class="squel-ligne"><span class="an-squelette squel-rond"></span><span class="squel-bloc"><span class="an-squelette" style="height:13px;width:38%;border-radius:3px"></span><span class="an-squelette" style="height:10px;width:55%;border-radius:3px"></span></span><span class="an-squelette" style="height:13px;width:70px;border-radius:3px"></span></div></li>
  </ul>
</div>
.an-bascule-charge .liste li { animation: an-fondu-entree 320ms var(--e-douce) both; }
.an-bascule-charge .liste li:nth-child(2) { animation-delay: 60ms; }
@keyframes an-fondu-entree { from { opacity: 0; transform: translateY(6px); } }
memes dimensions que le contenu reel, sinon la page saute
  1. Devis genereTermine
  2. Converti en Factur-XTermine
  3. Envoi au clientEn cours
  4. 4
    Attente de signatureA venir
Progression par etapes

Un traitement long en plusieurs phases. Le trait de liaison se remplit entre deux etapes : c'est lui qui donne le sentiment d'avancer, plus que la pastille.

Voir le codeMasquer le code
<ol class="an-etapes" style="--fait:2">
  <li class="f"><i><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20 6-11 11-5-5"/></svg></i><div><strong>Devis genere</strong><small>Termine</small></div></li>
  <li class="f"><i><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20 6-11 11-5-5"/></svg></i><div><strong>Converti en Factur-X</strong><small>Termine</small></div></li>
  <li class="c"><i><span class="btn__spin an-spin"></span></i><div><strong>Envoi au client</strong><small>En cours</small></div></li>
  <li><i>4</i><div><strong>Attente de signature</strong><small>A venir</small></div></li>
</ol>
.an-etapes { list-style: none; margin: 0; padding: 0; }
.an-etapes li { position: relative; display: flex; gap: var(--sp-4); padding-bottom: var(--sp-6); }
.an-etapes li:last-child { padding-bottom: 0; }
.an-etapes li::before {
  content: "";
  position: absolute;
  left: 15px; top: 32px; bottom: 0;
  width: 2px;
  background: var(--c-border);
}
.an-etapes li:last-child::before { display: none; }
.an-etapes li::after {
  content: "";
  position: absolute;
  left: 15px; top: 32px; bottom: 0;
  width: 2px;
  background: var(--c-success);
  transform-origin: 50% 0;
  transform: scaleY(0);
  transition: transform 500ms var(--e-douce);
}
.an-etapes li.f::after { transform: scaleY(1); }
.an-etapes li:nth-child(2).f::after { transition-delay: 300ms; }
.an-etapes li > i {
  display: flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; flex: none;
  border-radius: 50%;
  background: var(--c-surface-3);
  color: var(--c-text-muted);
  font-style: normal; font-size: var(--fs-xs); font-weight: var(--fw-bold);
  z-index: 1;
  transition: background-color 300ms var(--e-douce), color 300ms var(--e-douce);
}
.an-etapes li > i svg { width: 15px; height: 15px; }
.an-etapes li.f > i { background: var(--c-success); color: #fff; }
.an-etapes li.c > i { background: var(--c-primary-soft); }
.an-etapes li.c > i .an-spin { width: 16px; height: 16px; border-width: 2px; }
.an-etapes strong { display: block; font-size: var(--fs-md); font-weight: var(--fw-semi); }
.an-etapes small { font-size: var(--fs-xs); color: var(--c-text-muted); }
le trait de liaison est un ::after en scaleY anime, pas une hauteur

E. Attirer l'attention

Secousse d'erreur, surlignage d'arrivee, reflet de passage, halo d'appel. Les quatre seuls cas ou l'animation sert a interrompre.

Secousse d'erreur

Un envoi refuse. La secousse est breve et de faible amplitude : au-dela de 6 px et 400 ms, elle passe de « attention » a « agacant ».

Voir le codeMasquer le code
<div class="demo-row">
  <button class="btn btn--destructif an-secousse" data-an-rejouer>Mot de passe incorrect</button>
  <div class="champ is-erreur an-secousse" data-an-rejouer style="max-width:220px"><input class="saisie" value="dupont@exemple"></div>
</div>
.an-secousse { animation: an-secouer 400ms var(--e-douce); }
@keyframes an-secouer {
  0%, 100% { transform: translateX(0); }
  15% { transform: translateX(-6px); }
  30% { transform: translateX(5px); }
  45% { transform: translateX(-4px); }
  60% { transform: translateX(3px); }
  80% { transform: translateX(-2px); }
}
translateX de +-5 px, 5 alternances, 380 ms
Surlignage d'arrivee

Une ligne qui vient d'etre ajoutee ou modifiee par quelqu'un d'autre. Le fond se teinte puis s'efface en deux secondes : assez pour etre vu, assez court pour ne pas rester.

Voir le codeMasquer le code
<ul class="liste" data-an-rejouer>
  <li class="an-neuf"><a href="#"><span class="liste__bloc"><strong>Devis 2026-124</strong><small>Ajoute a l'instant par Sophie M.</small></span><span class="doc-etiq doc-etiq--ok">Nouveau</span></a></li>
  <li><a href="#"><span class="liste__bloc"><strong>Devis 2026-121</strong><small>Il y a 2 heures</small></span></a></li>
  <li><a href="#"><span class="liste__bloc"><strong>Devis 2026-118</strong><small>Hier</small></span></a></li>
</ul>
.an-neuf { animation: an-surligner 2.4s var(--e-douce) forwards; }
@keyframes an-surligner {
  0%   { background: var(--c-success-soft); }
  30%  { background: var(--c-success-soft); }
  100% { background: transparent; }
}
background-color anime de la teinte vers transparent en 2 s

Formule Pro

Relances automatiques et application mobile.

Formule Essentielle

Devis et factures illimites.

Reflet de passage

Un eclat qui traverse une carte pour signaler une nouveaute. A n'utiliser qu'une fois par ecran, et jamais en boucle : le regard s'y accroche et ne lit plus le reste.

Voir le codeMasquer le code
<div class="cartes-grille">
  <div class="carte an-reflet" data-an-rejouer><div class="carte__corps"><h3 class="carte__titre">Formule Pro</h3><p class="carte__texte">Relances automatiques et application mobile.</p></div></div>
  <div class="carte"><div class="carte__corps"><h3 class="carte__titre">Formule Essentielle</h3><p class="carte__texte">Devis et factures illimites.</p></div></div>
</div>
.an-reflet { position: relative; overflow: hidden; }
.an-reflet::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, transparent 30%, color-mix(in srgb, var(--c-primary) 22%, transparent) 50%, transparent 70%);
  transform: translateX(-120%);
  animation: an-balayer 1.6s var(--e-douce) .4s;
  pointer-events: none;
}
@keyframes an-balayer { to { transform: translateX(120%); } }
::after en gradient translate de -120% a 120%, une seule fois
Halo d'appel

Un bouton qu'on veut faire remarquer sans le faire clignoter. L'anneau se dilate et s'efface : le mouvement est en peripherie de vision, il n'empeche pas de lire.

Voir le codeMasquer le code
<div class="demo-row" style="padding:16px 0">
  <button class="btn btn--plein btn--lg btn--pilule an-halo">Demander un rappel</button>
  <span class="an-point-halo" aria-hidden="true"></span>
</div>
.an-halo { position: relative; }
.an-halo::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 2px solid var(--c-primary);
  animation: an-halo 2.2s var(--e-douce) infinite;
  pointer-events: none;
}
@keyframes an-halo {
  0%   { opacity: .7; transform: scale(1); }
  70%  { opacity: 0; transform: scale(1.3); }
  100% { opacity: 0; transform: scale(1.3); }
}
.an-point-halo { position: relative; width: 10px; height: 10px; border-radius: 50%; background: var(--c-danger); }
.an-point-halo::after {
  content: "";
  position: absolute; inset: 0;
  border-radius: 50%;
  background: var(--c-danger);
  animation: an-halo 1.8s var(--e-douce) infinite;
}
un anneau en ::after, scale(1) vers scale(1.4) avec opacity vers 0

F. Texte

Machine a ecrire, mots qui defilent, texte qui se decode, compteur. Le texte complet reste toujours dans le balisage.

Nous posons des pompes a chaleur depuis 2009.

Machine a ecrire

L'ouverture d'une page ou une demonstration. Le texte complet est dans le balisage : la lecture assistee et les moteurs de recherche voient la phrase entiere.

Voir le codeMasquer le code
<p class="an-frappe" data-an-frappe>Nous posons des pompes a chaleur depuis 2009.</p>
.an-frappe {
  margin: 0;
  font-size: clamp(20px, 3vw, 30px);
  font-weight: 700;
  letter-spacing: -.02em;
  min-height: 1.4em;
}
.an-frappe::after {
  content: "";
  display: inline-block;
  width: 2px;
  height: .95em;
  margin-left: 3px;
  vertical-align: -.12em;
  background: var(--c-primary);
  animation: an-curseur 1s steps(2) infinite;
}
@keyframes an-curseur { 50% { opacity: 0; } }
JS : on decoupe en caracteres, le curseur est un ::after clignotant

Nous posons des pompes a chaleurchauffe-eauchaudieresclimatisations depuis 2009.

Mots qui defilent

Une accroche dont un mot change. Le conteneur garde la largeur du mot le plus long, sinon la phrase se reorganise a chaque changement.

Voir le codeMasquer le code
<p class="an-defile-mots">
  Nous posons des
  <span class="an-defile" data-an-defile><i>pompes a chaleur</i><i>chauffe-eau</i><i>chaudieres</i><i>climatisations</i></span>
  depuis 2009.
</p>
.an-defile-mots { margin: 0; font-size: clamp(19px, 2.8vw, 28px); font-weight: 700; letter-spacing: -.02em; line-height: 1.5; }
.an-defile {
  display: inline-grid;
  vertical-align: bottom;
  height: 1.5em;
  overflow: hidden;
  color: var(--c-primary);
}
.an-defile i {
  grid-area: 1 / 1;
  font-style: normal;
  white-space: nowrap;
  transition: transform 520ms var(--e-douce), opacity 320ms var(--e-douce);
}
hauteur figee + translateY par pas d'une ligne, en boucle

RENOVATION ENERGETIQUE

Texte qui se decode

Un titre qui se stabilise a partir de caracteres aleatoires. Tres marque : a reserver a une page unique, et jamais sur une information qu'il faut lire vite.

Voir le codeMasquer le code
<p class="an-decode" data-an-decode="RENOVATION ENERGETIQUE">RENOVATION ENERGETIQUE</p>
.an-decode {
  margin: 0;
  font-family: var(--font-mono);
  font-size: clamp(17px, 3vw, 30px);
  font-weight: 700;
  letter-spacing: .02em;
  color: var(--c-text);
  min-height: 1.3em;
}
JS : chaque lettre se fixe apres n tirages, de gauche a droite
1 240installations depuis 2009
4,8note moyenne sur 312 avis
98pour cent de chantiers dans les delais
Compteur qui grimpe

Un chiffre de preuve. La valeur finale est dans le balisage, l'animation ne fait que la rejouer : si le script ne s'execute pas, le chiffre reste juste.

Voir le codeMasquer le code
<div class="an-chiffres" data-an-rejouer>
  <div><strong data-an-compte="1240">1 240</strong><span>installations depuis 2009</span></div>
  <div><strong data-an-compte="4.8" data-an-dec="1">4,8</strong><span>note moyenne sur 312 avis</span></div>
  <div><strong data-an-compte="98">98</strong><span>pour cent de chantiers dans les delais</span></div>
</div>
.an-chiffres { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--sp-5); text-align: center; }
.an-chiffres strong { display: block; font-size: 42px; font-weight: 800; letter-spacing: -.035em; font-variant-numeric: tabular-nums; line-height: 1; }
.an-chiffres span { display: block; margin-top: 8px; font-size: var(--fs-sm); color: var(--c-text-muted); line-height: 1.45; }
JS : sortie amortie en 1 - (1 - t)^3, chiffres tabulaires