Liquid✨ Premium

Button Morph

Un seul bouton qui raconte trois états : au clic, la pilule de 180 px se rétracte en cercle de 52 px, sa bordure tourne pendant l'attente, puis il devient une pastille verte où une coche apparaît en rebond, avant de redevenir bouton. Le changement de forme est l'indicateur d'état ; un second clic pendant la séquence est ignoré, et les lecteurs d'écran entendent « Envoi en cours… » puis « Envoyé ».

CSSJSUX

Mis à jour le

Cet effet fait partie d'Effect.Labs — 840 effets vanilla, une partie gratuite, une partie premium. Liquid comprend 31 effets, dont 6 gratuits. Découvrir la catégorie →

3 exemples d'usage

Lancer la machine n° 4 — appli de la laverie connectée d'une résidence étudiante — Button Morph exemple 1

① Lancer la machine n° 4 — appli de la laverie connectée d'une résidence étudiante

QuandSur la fiche « Machine 4 — 8 kg — 40 °C », l'étudiant appuie sur « Lancer » : le bouton se rétracte en cercle pendant que la machine répond, puis passe au vert, et la fin du cycle s'affiche au-dessus.
PourquoiLe cercle qui tourne couvre le délai réel de la machine et le bouton ignore les clics répétés : l'étudiant ne lance pas deux cycles par impatience.
RéglagesBouton d'origine (180 px, violet) centré sous la fiche ; data-busy="Lancement de la machine 4…", data-done="Machine 4 lancée" ; message « fin à 15 h 52 » affiché par la page pendant la phase .success.
Pointer son arrivée sur le chantier — appli de pointage d'une entreprise de maçonnerie — Button Morph exemple 2

② Pointer son arrivée sur le chantier — appli de pointage d'une entreprise de maçonnerie

QuandSur le téléphone du chef d'équipe : « Chantier Les Tilleuls — 7 h 32 », la météo, l'équipe du jour et un gros bouton « Pointer mon arrivée ».
PourquoiLa coche verte qui remplace le bouton se voit à bout de bras, avec des gants, en plein soleil ; un simple texte « enregistré » passerait inaperçu.
RéglagesLargeur au repos portée à 240 px par .btn-morph:not(.loading):not(.success), pour garder l'animation de largeur ; dégradé ambre #f59e0b → #d97706, anneau ambre ; écran clair de 320 px de large.
Déclarer la naissance d'un veau — appli de traçabilité d'un éleveur — Button Morph exemple 3

③ Déclarer la naissance d'un veau — appli de traçabilité d'un éleveur

QuandLe formulaire est rempli (numéro et nom de la mère, date, sexe, race, poids) ; l'éleveur appuie sur « Déclarer la naissance », le numéro de boucle attribué s'affiche à la réponse.
PourquoiLa déclaration a un délai légal : la séquence attente puis coche montre que la base a bien répondu, sans ouvrir de fenêtre.
RéglagesBouton de 250 px au repos, dégradé vert #16a34a → #15803d et anneau vert ; numéro de boucle en pastille verte en haut à droite pendant la phase .success.

Comment ça marche

Le bouton .btn-morph (180 px de large au minimum, 52 px de haut, dégradé violet) contient son texte et une coche SVG réduite à zéro. Au clic, le script ajoute .loading : min-width et width passent à 52 px, les coins à 50 %, le fond devient transparent et une bordure de 3 px apparaît, violette en haut, pâle ailleurs. Après 1,5 s, .loading cède la place à .success : pastille verte (#10b981 → #059669), coche qui grandit de 0 à 1 avec rebond. Encore 1,5 s, et le bouton reprend sa forme. Une variable running ignore les clics pendant ces 3 s : impossible de lancer deux fois la même action par impatience.

Le rétrécissement passe par une seule transition, .5s cubic-bezier(.68, -.55, .27, 1.55), qui fait d'abord gonfler légèrement le bouton avant qu'il se resserre et dépasse un peu sa taille finale. C'est min-width qui porte l'animation : une largeur auto ne s'interpole pas, alors qu'une largeur minimale de 180 px vers 52 px, si. La rotation de l'anneau (fx0404-spin, un tour en 0,8 s) attend 0,5 s, la fin du rétrécissement : sans ce délai, la pilule encore large tournerait sur elle-même comme une hélice pendant le premier quart de seconde.

L'état n'est pas que visuel. Pendant l'attente, le bouton porte aria-busy="true", et une zone .btn-morph-status en aria-live="polite", invisible à l'écran, reçoit le texte de data-busy (« Envoi en cours… ») puis celui de data-done (« Envoyé ») ; elle est vidée au retour, pour que l'annonce reparte au clic suivant. Le bouton a type="button" : placé dans un formulaire, il ne l'envoie pas tout seul. Les deux setTimeout simulent la réponse d'un serveur : remplacez-les par votre requête et passez en .success quand elle répond.

Accessibilité

  • Lecteurs d'écran : mesuré dans Chromium, Firefox et WebKit, la zone annoncée contient « Envoi en cours… » à 0,4 s, « Envoyé » à 1,7 s, puis rien après le retour. Adaptez data-busy et data-done à votre action (« Lancement de la machine… », « Machine lancée »).
  • Clavier : c'est un <button> natif, atteint par Tab et activé par Entrée ou Espace, avec un anneau de focus violet clair (:focus-visible). Pendant la séquence, le focus reste sur le bouton ; les appuis répétés sont ignorés.
  • Mouvement réduit : sous prefers-reduced-motion: reduce, le bouton prend directement sa forme de cercle (52 px dès la première mesure, à 150 ms), l'anneau reste fixe au lieu de tourner, la coche et les couleurs apparaissent en fondu de 0,3 s, et le bouton ne se soulève plus au survol.
  • Couleur : la réussite n'est pas dite que par le vert, la coche et l'annonce la confirment. Si l'action peut échouer, ajoutez un état d'erreur avec un texte, pas seulement une couleur rouge.

Compatibilité navigateur

CSS (transitions, @keyframes) + 1,2 Ko de JavaScript, aucune dépendance. Mesuré dans Chromium 151, Firefox 153 et WebKit 26 : même séquence (cercle à 0,4 s, pastille verte vers 1,5 s, bouton à 3 s), mêmes annonces, même arrêt de la rotation sous mouvement réduit. La police demandée est Inter : si votre page ne la charge pas, le texte s'affiche dans la police sans empattement du système.

Chrome 60+✓ Complet
Firefox 60+✓ Complet
Safari 12+✓ Complet
Edge 79+✓ Complet
Mobile iOS 12+✓ Complet (au doigt)
Android Chrome 60+✓ Complet (au doigt)

Sans JavaScript, c'est un bouton normal qui ne change pas de forme : branchez votre action sur un vrai formulaire pour qu'elle fonctionne quand même. Sans transitions CSS, les trois états se succèdent sans animation.

Le code

Structure HTML à coller dans votre page (le CSS + JS sont inclus avec votre achat) :

index.html — structure
<div class="fx-0404-root">
  <button type="button" class="btn-morph" data-busy="Envoi en cours…" data-done="Envoyé">
    <span class="btn-text">Soumettre</span>
    <svg class="check-icon" viewBox="0 0 24 24" aria-hidden="true">…</svg>
  </button>
  <span class="btn-morph-status" aria-live="polite"></span>
</div>

Paiement unique, sans abonnement — le code est à toi pour toujours

Personnaliser

Options passées à l'API ou attributs data-* :

Option / propriétéDéfautEffet
Durées des états (JS — les deux setTimeout(…, 1500)) 1,5 s d'attente, 1,5 s de réussite Avec une vraie requête, supprimez le premier délai et passez en .success dans le .then(). Gardez au moins 1 s de réussite pour que la coche soit vue.
Messages annoncés (HTML — data-busy / data-done) « Envoi en cours… » / « Envoyé » Décrivez l'action réelle : « Pointage en cours… » / « Arrivée enregistrée ». Ces textes ne s'affichent pas, ils sont lus par les lecteurs d'écran.
Largeur au repos (CSS — min-width de .btn-morph) 180 px Pour un libellé long, élargissez par .btn-morph:not(.loading):not(.success) { min-width: 240px } : l'animation part de cette largeur. Un texte plus large que min-width fait sauter le bouton à cette valeur avant qu'il se resserre.
Couleurs (CSS — background de .btn-morph et de .success, border-color de .loading) violet #a855f7 → #7c3aed, vert #10b981 → #059669 Accordez l'anneau à la couleur du bouton (première couleur pleine, les trois autres à 30 %) pour que le passage de l'un à l'autre se lise comme le même objet.
Vitesse de l'anneau (CSS — animation: .8s linear .5s infinite … fx0404-spin) un tour en 0,8 s, après 0,5 s 1,2 s donne une attente plus calme. Gardez le délai égal à la durée de la transition, sinon la pilule tourne avant d'être ronde.

FAQ

Remplacez le corps du clic par : btn.classList.add('loading'); fetch(url, { method: 'POST' }).then(function (r) { btn.classList.remove('loading'); btn.classList.add(r.ok ? 'success' : 'error'); });, puis retirez .success au bout de 1,5 s comme le fait le code. Gardez la variable running et les annonces data-busy / data-done. Une classe .error (fond rouge et texte « Réessayer ») est à ajouter dans le CSS si l'action peut échouer.
Parce que le bouton met 0,5 s à passer de pilule à cercle. Si la rotation démarrait au clic, c'est la pilule encore large, avec sa nouvelle bordure, qui tournerait sur elle-même : une barre qui balaie la page pendant un quart de seconde. Le délai de 0,5 s de fx0404-spin fait commencer la rotation quand la forme est ronde ; à l'œil, l'attente reste continue.
La largeur animée est min-width, de 180 px à 52 px. Si votre texte demande 220 px, le bouton mesure 220 px au repos, mais au clic il saute d'abord à 180 px puis se resserre : un petit à-coup. Donnez au bouton une largeur minimale au repos au moins égale à son texte, avec :not(.loading):not(.success) pour ne pas gêner les deux autres états, comme dans les exemples du chantier et de l'éleveur.