TransitionsGratuit

Popover Bloom

Une bulle qui éclot depuis sa pointe, juste au-dessus du bouton qui l'ouvre : un vrai popover HTML (Échap et clic à l'extérieur la ferment sans script), une entrée écrite avec @starting-style — opacité et scale de 0,6 à 1 en 0,4 s sur une courbe à rebond —, un ancrage CSS par position-area avec repli en JavaScript, et un fondu de 0,15 s quand l'utilisateur réduit les animations.

CSS@starting-stylePopover

Mis à jour le

La bulle éclot depuis son ancre
Survolez ou cliquez la scène pour interagir

Cet effet est gratuit — la catégorie Transitions en contient 52 au total, dont 9 gratuits. Effect.Labs regroupe 840 effets vanilla. Découvrir la catégorie →

Exemples d'usage

Fournil Lemaire · boulangerie en ligne

Pain aux graines

500 g · levain · cuit ce matin

4,20 €

Ajouter au panier

Ingrédients

Farine de blé T80, eau, levain naturel, (sésame, lin, tournesol), sel de Guérande.

Allergène : sésameGraines de sésame, lin et tournesol

① Allergènes d'un ingrédient — fiche produit d'une boulangerie en ligne

QuandSur la fiche du pain aux graines, le client touche le mot « graines » dans la liste des ingrédients : la bulle « Allergène : sésame » s'ouvre juste au-dessus du mot.
PourquoiLa bulle sort du mot touché : on sait à quel ingrédient elle se rapporte sans flèche ni surlignage, et la liste reste lisible autour.
RéglagesMot déclencheur en <button> sans fond, souligné en pointillés ; bulle ambrée (linear-gradient(135deg, #b45309, #c2410c), flèche #c2410c) sur deux lignes, titre en gras ; le rebond et l'échelle de départ restent ceux du code.
Tableau périodique · périodes 1 à 4Physique-chimie · 2de
H He Li Be B C N O F Ne Na Mg Al Si P S Cl Ar K Ca Sc Ti V Cr Mn Co Ni Cu Zn Ga Ge As Se Br Kr
Fe – 55,85fer · masse atomique (u)

alcalinstransitionnon-métauxgaz nobles

Touchez une case en surbrillance

Exercice 3 · masse molaireQuelle est la masse de 0,25 mol de fer ? Arrondir au dixième de gramme.

② Masse atomique d'un élément — tableau périodique d'une appli de chimie pour lycéens

QuandPendant un exercice sur la masse molaire, l'élève touche la case du fer : la bulle « Fe – 55,85 · fer, masse atomique (u) » éclot au-dessus de la case, dans un tableau de cases de 26 px.
PourquoiL'éclosion depuis la case garde le lien visuel avec l'élément dans une grille serrée, et la bulle passe au-dessus des cases voisines sans les décaler.
RéglagesCase du fer en <button> de 26 × 38 px avec un liseré bleu clair ; bulle bleu acier (#1d4ed8 → #0369a1) au texte centré, sous-titre en <small> ; la grille de 18 colonnes est un simple display: grid.
GESTION ARDOISEEspace locataire · mon bail

Article 4 — Conditions financières

Le loyer est fixé à 640 € par mois, auquel s'ajoutent 55 € de provisions sur charges, régularisées chaque année.

Il est révisé chaque année à la date anniversaire du bail, selon l'indice de référence des loyers (IRL).

À la signature, le locataire verse un de 640 €, restitué après la remise des clés.

Le loyer est payable par virement avant le 5 de chaque mois.

Dépôt de garantieAu plus un mois de loyer hors charges pour un logement vide. Rendu sous un mois si l'état des lieux de sortie est conforme, deux mois sinon.

③ Définition d'un terme du bail — espace locataire d'une agence de gestion

QuandLe locataire relit l'article 4 de son bail et touche « dépôt de garantie », souligné dans le texte : la définition s'ouvre au-dessus du terme.
PourquoiLe rebond léger rend l'aide sympathique sur un texte austère, et la bulle s'ouvre au-dessus de la ligne lue sans la recouvrir.
RéglagesBulle de 270 px de large qui passe à la ligne (white-space: normal), texte de 12,5 px ; teinte vert pétrole (#115e59 → #0f766e) ; terme en gras souligné en pointillés dans un texte en serif.

Comment ça marche

Le HTML tient en deux éléments : un <button popovertarget="popoverBloomBox"> et une div.popover-bloom-box qui porte l'attribut popover. Le navigateur fait le reste : un clic sur le bouton ouvre la bulle, un deuxième clic, la touche Échap ou un clic n'importe où ailleurs la referme (mode « auto », mesuré dans Chromium 151, Firefox 153 et WebKit 26.5). Ouverte, la bulle passe dans la couche supérieure de la page : aucun overflow: hidden d'un parent ne la coupe et aucun z-index voisin ne passe devant. Pour la poser au-dessus du bouton, le CSS remet inset: auto et margin: 0 0 10px puis demande position-area: top : la bulle se centre au-dessus de son ancre, à 10 px de son bord (mesuré dans les trois moteurs, écart horizontal 0 px).

L'éclosion repose sur deux états. Au repos, la bulle vaut opacity: 0 et scale: 0.6, avec transform-origin: bottom center : elle grandit depuis sa pointe, là où se trouve le mot touché. Ouverte (:popover-open), elle vaut 1 et 1. Comme un popover passe de display: none à affiché, il n'a pas d'état « avant » : la règle @starting-style lui en donne un (0 et 0,6), et la transition de 0,4 s sur cubic-bezier(0.34, 1.56, 0.64, 1) joue l'entrée avec un léger dépassement. Mesuré 70 ms après le clic : opacité 0,49 à 0,62 et échelle 0,80 à 0,85 selon le moteur. La sortie demande en plus display et overlay en allow-discrete, pour que la bulle reste affichée et dans la couche supérieure le temps de rétrécir : Chromium la joue (opacité 0,33 à 80 ms), Firefox 153 et WebKit 26.5 ne connaissent pas encore overlay et ferment la bulle d'un coup.

Le script (2,3 Ko) n'ouvre ni ne ferme rien. Il pose aria-expanded sur le bouton à chaque évènement toggle, et donne à chaque bulle de la page un nom d'ancre propre (--popover-bloom-1, -2…) : plusieurs bulles cohabitent sans se disputer le même bouton. Là où position-area manque, il place la bulle lui-même juste avant son ouverture (beforetoggle) à partir du rectangle du bouton, et la suit au défilement et au redimensionnement — mesuré en simulant l'absence d'ancrage : même écart de 10 px, bulle toujours au-dessus du bouton après défilement. Là où l'API Popover manque entièrement, il bascule une classe .popover-bloom-open et écoute Échap ; le clic à l'extérieur n'est alors pas géré.

Accessibilité

  • Clavier : le déclencheur est un vrai <button> — Entrée ou Espace ouvre la bulle, Échap la ferme, le focus reste sur le bouton (mesuré dans Chromium et Firefox ; Safari ne donne le focus aux boutons au clic que si l'utilisateur l'a réglé). Le bouton annonce son état : aria-expanded passe de false à true à l'ouverture et revient à la fermeture, quelle qu'en soit la cause (clic, Échap, clic extérieur).
  • Lecteurs d'écran : la bulle n'est pas annoncée d'elle-même quand elle s'ouvre. Gardez-la juste après son bouton dans le HTML, comme dans le code fourni : c'est l'élément suivant dans l'ordre de lecture. Fermée, elle est en display: none et n'est pas lue. Pour une information indispensable (un allergène, par exemple), écrivez-la aussi dans la page : une bulle reste un complément.
  • Mouvement réduit : sous prefers-reduced-motion, l'échelle reste à 1 et la bulle apparaît par un fondu de 0,15 s (mesuré dans les trois moteurs : opacité 0,58 à 0,74 et échelle 1 à 70 ms).
  • Contraste : texte blanc sur le dégradé #6366f1 → #8b5cf6, soit 4,47:1 au départ et 4,23:1 à l'arrivée, juste sous le seuil AA de 4,5:1 pour un texte de 13,6 px. Foncez le dégradé (#4f46e5 → #7c3aed : 6,29 et 5,70:1) si la bulle porte une information importante ; les trois scènes d'exemple restent au-dessus de 5:1 là où se trouve le texte.

Compatibilité navigateur

Attribut popover et popovertarget, @starting-style, propriété scale, transition-behavior: allow-discrete, ancrage CSS (position-area) + 2,3 Ko de JavaScript, aucune dépendance. Mesuré dans Chromium 151, Firefox 153 et WebKit 26.5 : ouverture, ancrage à 10 px, Échap, clic extérieur, aria-expanded et deux bulles dans la même page identiques ; sortie animée seulement dans Chromium, seul à connaître la propriété overlay. Repères de version publics : API Popover depuis Chrome 114, Firefox 125 et Safari 17 ; @starting-style depuis Chrome 117, Firefox 129 et Safari 17.5 ; position-area depuis Chrome 129, Firefox 147 et Safari 26 — avant, le script place la bulle.

Chrome 117+✓ Complet (ancrage CSS dès 129, placement par le script avant)
Firefox 129+✓ Entrée animée ; fermeture sans animation ; ancrage CSS dès 147
Safari 17.5+✓ Entrée animée ; fermeture sans animation ; ancrage CSS dès 26
Edge 117+✓ Complet
Mobile iOS 17.5+✓ Comme Safari ; un toucher à côté ferme la bulle à partir d'iOS 18.3 (avant : bouton ou Échap)
Android Chrome 117+✓ Complet

Sans @starting-style (Firefox 125-128, Safari 17.0-17.4), la bulle s'ouvre et se ferme d'un coup, sans éclosion. Sans ancrage CSS, le script la place au-dessus du bouton et la suit au défilement. Sans API Popover (Safari avant 17, Firefox avant 125), le script bascule une classe : la bulle éclot toujours et Échap la ferme, mais un clic à l'extérieur ne la ferme pas et elle peut passer sous un élément voisin. Sans JavaScript, le popover natif s'ouvre et se ferme quand même ; il ne manque que l'ancrage là où le navigateur ne le connaît pas, et aria-expanded.

Le code

Copiez les trois blocs dans votre page. Aucune dépendance.

HTML
<div class="popover-bloom-stage">
  <button class="popover-bloom-btn" type="button" popovertarget="popoverBloomBox">Afficher la bulle</button>
  <div class="popover-bloom-box" id="popoverBloomBox" popover>La bulle éclot depuis son ancre</div>
</div>
CSS
.popover-bloom-stage,
.popover-bloom-stage *,
.popover-bloom-stage *::before,
.popover-bloom-stage *::after {
  box-sizing: border-box
}

.popover-bloom-stage {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.popover-bloom-btn {
  padding: 10px 24px;
  font-size: 0.85rem;
  font-weight: 600;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-family: inherit;
  background: #6366f1;
  color: white;
  transition: background 0.2s, transform 0.1s;
}

.popover-bloom-btn:hover {
  background: #4f46e5;
}

.popover-bloom-btn:active {
  transform: scale(0.96);
}

/* La bulle est un popover : elle vit dans la couche supérieure (jamais coupée par un overflow: hidden,
   jamais sous un z-index voisin) et se place au-dessus du bouton qui l'ouvre grâce à l'ancrage CSS
   (position-area). Sans ancrage CSS, le script la place lui-même. */
.popover-bloom-box {
  position: fixed;
  inset: auto;
  margin: 0 0 10px;
  position-area: top;
  border: 0;
  overflow: visible;
  background: linear-gradient(135deg, #6366f1, #8b5cf6);
  color: white;
  padding: 14px 22px;
  border-radius: 12px;
  font-family: sans-serif;
  font-size: 0.85rem;
  font-weight: 500;
  white-space: nowrap;
  box-shadow: 0 10px 40px rgba(99, 102, 241, 0.4);
  opacity: 0;
  scale: 0.6;
  pointer-events: none;
  transform-origin: bottom center;
  transition: opacity 0.4s cubic-bezier(0.34, 1.56, 0.64, 1),
    scale 0.4s cubic-bezier(0.34, 1.56, 0.64, 1),
    display 0.4s allow-discrete,
    overlay 0.4s allow-discrete;
}

.popover-bloom-box::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 8px solid transparent;
  border-top-color: #8b5cf6;
}

/* Ouverte : état d'arrivée. @starting-style donne l'état de départ de l'entrée (le popover
   passe de display: none à affiché, il n'a pas d'état « avant » sans cette règle). */
.popover-bloom-box:popover-open {
  opacity: 1;
  scale: 1;
  pointer-events: auto;
}

/* Repli des navigateurs sans API Popover (règle séparée : un sélecteur :popover-open inconnu
   invaliderait toute la liste). */
.popover-bloom-box.popover-bloom-open {
  opacity: 1;
  scale: 1;
  pointer-events: auto;
}

@starting-style {
  .popover-bloom-box:popover-open {
    opacity: 0;
    scale: 0.6;
  }
}

@media (prefers-reduced-motion: reduce) {
  .popover-bloom-box {
    scale: 1;
    transition: opacity 0.15s ease, display 0.15s allow-discrete, overlay 0.15s allow-discrete;
  }

  @starting-style {
    .popover-bloom-box:popover-open {
      scale: 1;
    }
  }
}
JavaScript (fx-0715)
(function() {
  /* Popover Bloom : l'attribut popovertarget ouvre et ferme la bulle sans script (Échap et clic
     à l'extérieur compris). Ce script ajoute aria-expanded, ancre chaque bulle à son bouton et
     prend le relais là où le navigateur ne connaît pas l'ancrage CSS ou l'API Popover. */
  var hasPopover = HTMLElement.prototype.hasOwnProperty('popover');
  var hasAnchor = !!(window.CSS && CSS.supports('position-area', 'top'));
  var count = 0;

  document.querySelectorAll('.popover-bloom-box').forEach(function(box) {
    var btn = box.id && document.querySelector('[popovertarget="' + box.id + '"]');
    if (!btn) return;
    btn.setAttribute('aria-expanded', 'false');

    function place() {
      var r = btn.getBoundingClientRect();
      box.style.left = (r.left + r.width / 2) + 'px';
      box.style.top = (r.top - 10) + 'px';
      box.style.translate = '-50% -100%';
      box.style.margin = '0';
    }

    if (!hasPopover) {
      /* Navigateur sans API Popover : bascule par classe, Échap ferme. */
      box.setAttribute('aria-hidden', 'true');
      var setOpen = function(open) {
        if (open) place();
        box.classList.toggle('popover-bloom-open', open);
        box.setAttribute('aria-hidden', String(!open));
        btn.setAttribute('aria-expanded', String(open));
      };
      btn.addEventListener('click', function() {
        setOpen(!box.classList.contains('popover-bloom-open'));
      });
      document.addEventListener('keydown', function(e) {
        if (e.key === 'Escape') setOpen(false);
      });
      return;
    }

    if (hasAnchor) {
      /* Nom d'ancre propre à chaque bulle : plusieurs bulles cohabitent dans la page. */
      var name = '--popover-bloom-' + (++count);
      btn.style.setProperty('anchor-name', name);
      box.style.setProperty('position-anchor', name);
    }

    box.addEventListener('beforetoggle', function(e) {
      if (e.newState === 'open' && !hasAnchor) place();
    });
    box.addEventListener('toggle', function(e) {
      btn.setAttribute('aria-expanded', String(e.newState === 'open'));
    });
    if (!hasAnchor) {
      var follow = function() {
        if (box.matches(':popover-open')) place();
      };
      window.addEventListener('scroll', follow, true);
      window.addEventListener('resize', follow);
    }
  });
})();

Personnaliser

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

Option / propriétéDéfautEffet
Côté (CSS — position-area et transform-origin) top et bottom center : au-dessus, éclosion depuis la pointe bottom avec transform-origin: top center ouvre la bulle sous le bouton ; déplacez alors la flèche (::after en bottom: 100%, border-bottom-color). Le repli du script place toujours au-dessus : adaptez place() si vous changez de côté.
Écart avec le bouton (CSS — margin: 0 0 10px) 10 px Gardez au moins la hauteur de la flèche (8 px). Le repli du script retire 10 px au haut du bouton : changez les deux valeurs ensemble.
Rebond (CSS — cubic-bezier(0.34, 1.56, 0.64, 1), 0,4 s) la bulle monte à l'échelle 1,04 avant de se poser à 1 cubic-bezier(0.2, 0, 0, 1) donne une ouverture nette sans rebond ; au-delà de 0,5 s, la bulle paraît lente pour une simple aide.
Échelle de départ (CSS — scale: 0.6 dans la règle de base et dans @starting-style) 0,6 0,85 pour une éclosion discrète, 0,3 pour une bulle qui sort presque d'un point. Changez les deux règles ensemble, sinon l'entrée et la sortie diffèrent.
Fermeture (HTML — popover ou popover="manual") popover (auto) : Échap et clic extérieur ferment, une seule bulle ouverte à la fois popover="manual" laisse plusieurs bulles ouvertes ensemble et ne ferme qu'au bouton : utile pour une visite guidée, à éviter pour une aide ponctuelle.
Texte long (CSS — white-space: nowrap) une ligne Pour une définition, passez à white-space: normal avec une largeur fixe (240 à 300 px) : la bulle reste centrée sur le mot.
Couleurs (CSS — background et border-top-color de la flèche) #6366f1 → #8b5cf6, flèche #8b5cf6 Donnez à la flèche la couleur de la fin du dégradé, sinon une marche apparaît sous la bulle.

FAQ

Pourquoi la bulle se ferme-t-elle sans animation dans Firefox et Safari ?

Pour s'animer en sortant, un popover doit rester affiché et rester dans la couche supérieure pendant qu'il rétrécit. Le code le demande avec display et overlay en allow-discrete. Chromium connaît les deux ; Firefox 153 et WebKit 26.5 ne connaissent pas overlay et retirent la bulle d'un coup (mesuré : opacité 0 dès 80 ms). L'entrée, elle, est animée partout grâce à @starting-style. Rien à changer de votre côté : la sortie s'animera quand ces navigateurs ajouteront overlay.

Quelle différence avec Morph Transition (fx-0721), qui utilise aussi @starting-style ?

Morph Transition anime un élément qui reste dans la page et passe vraiment en display: none : un champ de formulaire, un bandeau, un panneau de filtres qui libèrent leur place. Popover Bloom ouvre une bulle par-dessus la page, ancrée à un bouton, avec la fermeture native du popover (Échap, clic extérieur). Choisissez Popover Bloom pour une information ponctuelle liée à un mot ou une case ; Morph Transition pour un bloc qui apparaît ou disparaît dans le flux.

Pourquoi un popover plutôt qu'une div positionnée en absolu ?

Une div en position: absolute est coupée par le premier parent en overflow: hidden (une carte, un tableau qui défile) et peut passer sous un voisin qui a un z-index. Le popover s'affiche dans la couche supérieure : rien ne le coupe ni ne le recouvre. En prime, le navigateur gère Échap et le clic à l'extérieur, et une seule bulle « auto » reste ouverte à la fois. Le prix : sans ancrage CSS, il faut le placer en JavaScript, ce que fait le script fourni.