Les éléments qui s'affichent dans la "couche supérieure" (comme <dialog>, les éléments avec l'attribut popover ou les info-bulles) ont toujours été difficiles à animer, car ils basculent entre display: none et un état visible. Le CSS moderne fournit @starting-style, transition-behavior: allow-discrete et la propriété overlay pour permettre des transitions d'entrée et de sortie fluides pour ces éléments. Notez que l'imbrication CSS native est utilisée dans les exemples ci-dessous.
Implémentation
1. Activer les transitions discrètes
Pour animer la propriété display, vous devez définir transition-behavior: allow-discrete. Cela permet à l'élément de rester visible pendant sa transition de sortie. Si vous utilisez des raccourcis de transition, veillez à placer transition-behavior: allow-discrete après pour éviter que le raccourci ne l'annule.
2. La propriété overlay
Lorsqu'un élément entre ou sort de la couche supérieure, il doit effectuer une transition de la propriété overlay. Cela garantit que l'élément reste dans la couche supérieure pendant toute la durée de l'animation, ce qui l'empêche d'être coupé prématurément par d'autres éléments ou par la fenêtre d'affichage.
3. Animations d'entrée avec @starting-style
Utilisez la règle @ @starting-style pour définir les styles à partir desquels un élément doit effectuer une transition lorsqu'il est affiché pour la première fois ou lorsque sa propriété display passe de none à une autre valeur.
4. Animer le Backdrop
Le pseudo-élément ::backdrop peut être animé de la même manière en appliquant des transitions à ses propres propriétés.
Exemple
/* 1. Define the visible (open) state */
dialog[open],
[popover]:popover-open {
opacity: 1;
transform: scale(1);
/* 2. Define the starting state for entry (must come after open state) */
@starting-style {
opacity: 0;
transform: scale(0.9);
}
}
/* 3. Define the base (closed/exit) state and transitions */
dialog,
[popover] {
opacity: 0;
transform: scale(0.9);
/* MANDATORY: transition display and overlay for top-layer elements */
transition-property: opacity, transform, display, overlay;
transition-duration: 0.3s;
transition-timing-function: ease-out;
/* Applies to discrete properties like display and overlay */
transition-behavior: allow-discrete; /* Note: be sure to write this after the shorthand */
}
/* 4. Animate the backdrop */
dialog::backdrop,
[popover]::backdrop {
background-color: rgba(0, 0, 0, 0);
transition-property: background-color, display, overlay;
transition-duration: 0.3s;
transition-timing-function: ease-out;
transition-behavior: allow-discrete;
}
dialog[open]::backdrop,
[popover]:popover-open::backdrop {
background-color: rgba(0, 0, 0, 0.5);
@starting-style {
background-color: rgba(0, 0, 0, 0);
}
}
/* 5. Respect user preference for reduced motion */
@media (prefers-reduced-motion: reduce) {
dialog,
[popover] {
/* Disable movement and shorten duration for a simple fade */
transform: none;
transition-duration: 0.1s;
}
@starting-style {
dialog[open],
[popover]:popover-open {
transform: none;
}
}
}
Contraintes et accessibilité
- OBLIGATOIRE : incluez
overlaydans votre listetransitionpour tout élément entrant ou sortant de la couche supérieure. - OBLIGATOIRE : Utilisez
allow-discretepour la transition de la propriétédisplay. - OBLIGATOIRE : Respectez les préférences de l'utilisateur concernant la réduction des animations à l'aide de
prefers-reduced-motionen simplifiant les transitions (par exemple, en supprimant les transformations et en raccourcissant la durée). - À FAIRE : Placez le bloc
@starting-styleà l'intérieur ou après le sélecteur d'état "open" pour assurer une cascade correcte. - À ÉVITER : Utiliser
@starting-stylepour les animations de sortie. Les animations de sortie sont définies par la transition vers l'état de base (fermé).
Stratégies de remplacement
Créations de remplacement et compatibilité des navigateurs avec transition-behavior
Firefox 129 et versions ultérieures analysent transition-behavior: allow-discrete (CSS.supports('transition-behavior', 'allow-discrete') renvoie true) sans effectuer de transition de la propriété display (bug Firefox 1882408), ce qui entraîne la disparition immédiate des éléments à la sortie.
Pour détecter de manière fiable la compatibilité avec les transitions display discrètes, vérifiez si la valeur display calculée d'un élément temporaire reste visible lors de la transition vers none, plutôt que de vous fier uniquement à CSS.supports('transition-behavior', 'allow-discrete') :
let supportsDisplayTransition;
function canTransitionDisplay() {
if (supportsDisplayTransition !== undefined) return supportsDisplayTransition;
if (!window.CSS?.supports?.('transition-behavior', 'allow-discrete') || !document.body) {
return false;
}
const probe = document.createElement('div');
// The shorthand is intentional here: browsers that don't parse allow-discrete
// drop the whole declaration, so display: none applies instantly and the probe
// correctly returns false. !important guards against global reduced-motion
// resets like `* { transition: none !important }`.
probe.style.cssText = 'transition: display 1s allow-discrete !important; display: block;';
document.body.appendChild(probe);
getComputedStyle(probe).display;
probe.style.display = 'none';
supportsDisplayTransition = getComputedStyle(probe).display === 'block';
probe.remove();
return supportsDisplayTransition;
}
Sortie de remplacement du calque du haut
Les animations d'entrée fonctionnent en CSS pur sur tous les navigateurs compatibles avec @starting-style. Aucune classe .is-opening n'est nécessaire, car les transitions d'entrée ne dépendent pas de overlay ni de transitions display discrètes.
Les animations de sortie nécessitent la prise en charge de overlay et de la transition display discrète. Si l'un ou l'autre n'est pas compatible (comme dans Firefox et Safari), encapsulez les sélecteurs d'état ouvert dans :is() et ajoutez :where(:not([data-closing])) (imbrication de &::backdrop et @starting-style) afin que la définition de data-closing déclenche la transition de sortie pendant que l'élément reste dans la couche supérieure, puis attendez que getAnimations() se stabilise avant d'appeler .close() ou .hidePopover() :
:is(dialog[open], [popover]:popover-open):where(:not([data-closing])) {
opacity: 1;
transform: scale(1);
@starting-style {
opacity: 0;
transform: scale(0.9);
}
&::backdrop {
background-color: rgb(0 0 0 / 0.5);
@starting-style {
background-color: transparent;
}
}
}
// Evaluate lazily: canTransitionDisplay() needs document.body, so a top-level
// const would be permanently false if this script runs in <head>.
function supportsTopLayerExit() {
return window.CSS?.supports?.('overlay', 'auto') && canTransitionDisplay();
}
async function closeTopLayer(element) {
if (!supportsTopLayerExit()) {
element.setAttribute('data-closing', '');
const animations = element.getAnimations({ subtree: true });
if (animations.length > 0) {
await Promise.race([
Promise.allSettled(animations.map((a) => a.finished)),
new Promise((r) => setTimeout(r, 2000)),
]);
}
if (!element.hasAttribute('data-closing')) return;
element.removeAttribute('data-closing');
}
element.close();
// Or for popover:
// element.hidePopover();
}
// Route native close requests (Esc, closedby light dismiss) through the same
// helper so they animate too, instead of closing instantly.
dialog.addEventListener('cancel', (event) => {
event.preventDefault();
closeTopLayer(dialog);
});
La fermeture d'un popover par clic en dehors de celui-ci et les boutons d'activation/désactivation popovertarget ne peuvent pas être interceptés (beforetoggle ne peut être annulé qu'à l'ouverture). Par conséquent, dans les navigateurs qui nécessitent la solution de remplacement, ces sorties sont instantanées. Fournissez une commande de fermeture explicite qui appelle closeTopLayer() si l'animation de sortie est importante.
Solutions de remplacement et compatibilité des navigateurs pour Popover
L'API Popover est principalement une amélioration progressive, mais ses comportements déterminants (promotion de la couche supérieure, fermeture par clic en dehors de l'élément et appel de popovertarget) n'ont pas d'équivalent CSS uniquement. Les anciens navigateurs ont besoin d'un polyfill ou d'un remplacement manuel si vous préférez ne pas en fournir.
Polyfill : pour que l'attribut popover soit compatible avec les anciens navigateurs, chargez @oddbird/popover-polyfill de manière conditionnelle. OBLIGATOIRE : Détectez la fonctionnalité en vérifiant la propriété popover sur HTMLElement.prototype, et chargez le polyfill uniquement lorsque la compatibilité native est manquante. Ne le chargez PAS de manière inconditionnelle.
Avec un bundler ou une carte d'importation :
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
Sans bundler, importez depuis un CDN dans un <script type="module"> :
<script type="module">
if (!("popover" in HTMLElement.prototype)) {
import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
}
</script>
Mise en garde concernant l'application de styles : le polyfill ne peut pas définir la véritable pseudo-classe :popover-open. Il applique donc une classe .\:popover-open à la place. OBLIGATOIRE : Combinez les deux avec :is() ou :where(), sinon les navigateurs qui ne disposent pas de :popover-open ignorent l'intégralité de la règle :
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
Vous pouvez également utiliser position: fixed pour une solution de remplacement sans polyfill et calculer manuellement les coordonnées via getBoundingClientRect(), ou vous appuyer sur le positionnement par défaut avec inset: auto si cela convient à votre cas d'utilisation.