Fournil Lemaire · online bakery
Seeded loaf
€4.20
Ingredients
T80 wheat flour, water, natural sourdough, (sesame, flax, sunflower), Guérande salt.
A bubble that blooms from its tip, right above the button that opens it: a real HTML popover (Escape and an outside click close it with no script), an entrance written with @starting-style — opacity and scale from 0.6 to 1 in 0.4 s on a bouncy curve —, CSS anchoring with position-area and a JavaScript fallback, and a 0.15 s fade when the user reduces motion.
Updated
This effect is free — the Transitions category contains 52 effects total, including 9 free. Effect.Labs has 840 vanilla effects. Explore the category →
Fournil Lemaire · online bakery
€4.20
T80 wheat flour, water, natural sourdough, (sesame, flax, sunflower), Guérande salt.
alkalitransitionnon-metalsnoble gases
Tap a highlighted cell
Exercise 3 · molar massWhat is the mass of 0.25 mol of iron? Round to the nearest 0.1 g.
The rent is set at €640 per month, plus €55 of service charges paid in advance and adjusted each year.
It is reviewed every year on the anniversary date of the lease, according to the rent reference index (IRL).
On signing, the tenant pays a of €640, returned after the keys are handed back.
Rent is paid by bank transfer before the 5th of each month.
The HTML is two elements: a <button popovertarget="popoverBloomBox"> and a div.popover-bloom-box carrying the popover attribute. The browser does the rest: a click on the button opens the bubble, a second click, the Escape key or a click anywhere else closes it ("auto" mode, measured in Chromium 151, Firefox 153 and WebKit 26.5). Once open, the bubble moves to the page's top layer: no parent's overflow: hidden clips it and no neighboring z-index covers it. To sit it above the button, the CSS resets inset: auto and margin: 0 0 10px, then asks for position-area: top: the bubble centers above its anchor, 10 px from its edge (measured in all three engines, 0 px horizontal offset).
The bloom relies on two states. At rest, the bubble is opacity: 0 and scale: 0.6, with transform-origin: bottom center: it grows from its tip, where the tapped word is. Open (:popover-open), it is 1 and 1. Since a popover goes from display: none to displayed, it has no "before" state: the @starting-style rule gives it one (0 and 0.6), and the 0.4 s transition on cubic-bezier(0.34, 1.56, 0.64, 1) plays the entrance with a slight overshoot. Measured 70 ms after the click: opacity 0.49 to 0.62 and scale 0.80 to 0.85 depending on the engine. The exit also needs display and overlay as allow-discrete, so the bubble stays displayed and in the top layer while it shrinks: Chromium plays it (opacity 0.33 at 80 ms), Firefox 153 and WebKit 26.5 do not know overlay yet and close the bubble at once.
The script (2.3 KB) opens and closes nothing. It sets aria-expanded on the button on every toggle event, and gives each bubble of the page its own anchor name (--popover-bloom-1, -2…): several bubbles coexist without fighting over the same button. Where position-area is missing, it places the bubble itself just before it opens (beforetoggle) from the button's rectangle, and follows it on scroll and resize — measured by simulating the lack of anchoring: same 10 px gap, bubble still above the button after scrolling. Where the Popover API is missing altogether, it toggles a .popover-bloom-open class and listens to Escape; outside clicks are not handled then.
<button> — Enter or Space opens the bubble, Escape closes it, focus stays on the button (measured in Chromium and Firefox; Safari only focuses buttons on click if the user has set it up). The button announces its state: aria-expanded goes from false to true on opening and back on closing, whatever the cause (click, Escape, outside click).display: none and is not read. For essential information (an allergen, for instance), also write it in the page: a bubble remains a complement.prefers-reduced-motion, the scale stays at 1 and the bubble appears with a 0.15 s fade (measured in all three engines: opacity 0.58 to 0.74 and scale 1 at 70 ms).popover and popovertarget attributes, @starting-style, the scale property, transition-behavior: allow-discrete, CSS anchoring (position-area) + 2.3 KB of JavaScript, no dependency. Measured in Chromium 151, Firefox 153 and WebKit 26.5: opening, 10 px anchoring, Escape, outside click, aria-expanded and two bubbles on the same page all identical; animated exit only in Chromium, the only one that knows the overlay property. Public version markers: Popover API since Chrome 114, Firefox 125 and Safari 17; @starting-style since Chrome 117, Firefox 129 and Safari 17.5; position-area since Chrome 129, Firefox 147 and Safari 26 — before that, the script places the bubble.
Without @starting-style (Firefox 125-128, Safari 17.0-17.4), the bubble opens and closes at once, with no bloom. Without CSS anchoring, the script places it above the button and follows it on scroll. Without the Popover API (Safari before 17, Firefox before 125), the script toggles a class: the bubble still blooms and Escape closes it, but an outside click does not close it and it can slip under a neighboring element. Without JavaScript, the native popover still opens and closes; only the anchoring is missing where the browser does not know it, and aria-expanded.
Copy the three blocks into your page. No dependencies.
<div class="popover-bloom-stage">
<button class="popover-bloom-btn" type="button" popovertarget="popoverBloomBox">Show the bubble</button>
<div class="popover-bloom-box" id="popoverBloomBox" popover>The bubble blooms from its anchor</div>
</div>
.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;
}
}
}
(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);
}
});
})();
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Side (CSS — position-area and transform-origin) |
top and bottom center: above, blooming from the tip |
bottom with transform-origin: top center opens the bubble under the button; then move the arrow (::after at bottom: 100%, border-bottom-color). The script fallback always places it above: adapt place() if you change sides. |
Gap with the button (CSS — margin: 0 0 10px) |
10 px | Keep at least the arrow's height (8 px). The script fallback subtracts 10 px from the button's top: change both values together. |
Bounce (CSS — cubic-bezier(0.34, 1.56, 0.64, 1), 0.4 s) |
the bubble rises to a 1.04 scale before settling at 1 | cubic-bezier(0.2, 0, 0, 1) gives a crisp opening with no bounce; beyond 0.5 s, the bubble feels slow for simple help. |
Starting scale (CSS — scale: 0.6 in the base rule and in @starting-style) |
0.6 | 0.85 for a subtle bloom, 0.3 for a bubble that almost comes out of a dot. Change both rules together, otherwise entrance and exit differ. |
Closing (HTML — popover or popover="manual") |
popover (auto): Escape and outside click close it, one bubble open at a time |
popover="manual" keeps several bubbles open together and only closes from the button: useful for a guided tour, to avoid for one-off help. |
Long text (CSS — white-space: nowrap) |
one line | For a definition, switch to white-space: normal with a fixed width (240 to 300 px): the bubble stays centered on the word. |
Colors (CSS — background and the arrow's border-top-color) |
#6366f1 → #8b5cf6, arrow #8b5cf6 |
Give the arrow the color of the gradient's end, otherwise a step shows under the bubble. |
To animate on its way out, a popover must stay displayed and stay in the top layer while it shrinks. The code asks for it with display and overlay as allow-discrete. Chromium knows both; Firefox 153 and WebKit 26.5 do not know overlay and remove the bubble at once (measured: opacity 0 from 80 ms). The entrance, however, is animated everywhere thanks to @starting-style. Nothing to change on your side: the exit will animate when these browsers add overlay.
Morph Transition animates an element that stays in the page and really goes to display: none: a form field, a banner, a filter panel that free their space. Popover Bloom opens a bubble on top of the page, anchored to a button, with the popover's native closing (Escape, outside click). Choose Popover Bloom for one-off information tied to a word or a cell; Morph Transition for a block that appears or disappears in the flow.
A position: absolute div is clipped by the first parent with overflow: hidden (a card, a scrolling table) and can slip under a neighbor with a z-index. The popover is shown in the top layer: nothing clips or covers it. As a bonus, the browser handles Escape and outside clicks, and only one "auto" bubble stays open at a time. The price: without CSS anchoring, it has to be placed in JavaScript, which the provided script does.