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é ».
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



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-busyetdata-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.
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) :
<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éfaut | Effet |
|---|---|---|
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
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.fx0404-spin fait commencer la rotation quand la forme est ronde ; à l'œil, l'attente reste continue.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.