Auparavant, les transitions CSS ne pouvaient pas animer les éléments lorsqu'ils étaient ajoutés au DOM pour la première fois ou lorsque leur propriété display passait de none. La règle @ @starting-style et transition-behavior: allow-discrete permettent de créer de manière déclarative des animations d'entrée et de sortie fluides.
Implémentation
1. Animation des boutons d'activation/de désactivation display: none
Pour animer un élément lorsque vous activez ou désactivez sa visibilité via un attribut (par exemple, hidden avec display: none) :
- Définir l'état visible : définissez les valeurs de propriété finales (par exemple,
opacity: 1) sur la classe de base. - Définir l'état de départ de l'entrée : utilisez
@starting-stylepour spécifier les valeurs à partir desquelles effectuer la transition lorsque l'élément devient visible. - Activer les transitions discrètes : incluez
displaydans la propriététransitionet utiliseztransition-behavior: allow-discrete. - Définir l'état de sortie : définissez les valeurs cibles dans l'attribut
hidden.
.card {
display: block;
opacity: 1;
translate: 0;
/* MANDATORY: Use transition-behavior: allow-discrete for display transition */
transition:
display 0.4s,
opacity 0.4s ease-out,
translate 0.4s ease-out;
transition-behavior: allow-discrete;
}
/* Entry animation: transition FROM these values when first rendered */
@starting-style {
.card {
opacity: 0;
translate: 0 -20px;
}
}
/* Exit animation: transition TO these values when hidden */
.card[hidden] {
display: none;
opacity: 0;
translate: 0 -20px;
}
/* Respect user preference for reduced motion */
@media (prefers-reduced-motion: reduce) {
.card {
/* Disable movement and shorten duration for a simple fade */
translate: none;
transition-duration: 0.1s;
}
@starting-style {
.card {
translate: none;
}
}
.card[hidden] {
translate: none;
}
}
2. Animer l'insertion et la suppression du DOM
Pour les éléments ajoutés via appendChild() ou supprimés via remove() :
- Entrée : utilisez
@starting-stylecomme indiqué ci-dessus. Le navigateur détectera automatiquement le changement de style de "rien" aux styles initiaux de l'élément et déclenchera la transition à partir des valeurs@starting-style. - Suppression : étant donné que
element.remove()est instantané et ne déclenche pas de transition CSS de lui-même, vous devez d'abord déclencher la transition de sortie et attendre qu'elle se termine avant de supprimer le nœud du DOM.
// 1. Trigger exit transition
element.hidden = true;
// 2. Wait for all active transitions/animations to finish,
// with a failsafe timeout in case an animation never ends (e.g. for looping animations)
const animations = element.getAnimations();
if (animations.length > 0) {
await Promise.race([
// Promise.allSettled ensures we wait even if some animations fail
Promise.allSettled(animations.map(a => a.finished)),
new Promise(r => setTimeout(r, 2000))
]);
}
// 3. Finally remove the node from the DOM
element.remove();
Contraintes et accessibilité
- OBLIGATOIRE : Utilisez
transition-behavior: allow-discretelors de la transition dedisplay. Sans cela, l'élément disparaîtra instantanément lors de la sortie. - N'UTILISEZ PAS
allow-discretedans la notation abrégéetransition, car les anciens navigateurs ignoreront l'intégralité de la déclarationtransition. Sauf dans les cas d'utilisation où cela est souhaitable, utilisez une déclarationtransition-behavior: allow-discretedistincte. - OBLIGATOIRE : Utilisez
@starting-stylepour les animations d'entrée. Les navigateurs ignorent les transitions lors de la première mise à jour du style d'un élément (rendu initial ou modification dedisplay: none), sauf si cette propriété est fournie. - À FAIRE : Incluez
overlaydans la listetransitionsi vous animez des éléments de la couche supérieure tels que<dialog>oupopoverpour vous assurer qu'ils restent dans la couche supérieure pendant l'animation de sortie. - À FAIRE : Respectez les préférences de l'utilisateur concernant la réduction des animations à l'aide de la requête média
prefers-reduced-motion. - À ÉVITER : Ne vous fiez pas à
@starting-stylepour les animations de sortie. Il ne définit que le point de départ d'une transition d'entrée. Les animations de sortie sont définies par la transition vers l'état masqué.
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;
}
Sortir de la création de remplacement lorsque les transitions display discrètes ne sont pas acceptées
Les animations d'entrée utilisant @starting-style fonctionnent sur tous les navigateurs récents sans JavaScript. Lorsque les transitions display discrètes ne sont pas prises en charge (!canTransitionDisplay()), la définition de hidden applique immédiatement display: none et ignore l'animation de sortie (à la fois lors du masquage et avant element.remove()). Séparez l'état de sortie visuel ([data-closing]) de display: none ([hidden]) pour que opacity et translate terminent l'animation avant de masquer ou de supprimer l'élément. Remplacez les règles .card[hidden] de l'étape 1 (y compris le remplacement prefers-reduced-motion) par :
.card:where([hidden], [data-closing]) {
opacity: 0;
translate: 0 -20px;
}
.card[hidden] {
display: none;
}
@media (prefers-reduced-motion: reduce) {
.card:where([hidden], [data-closing]) {
translate: none;
}
}
async function hideElement(el) {
if (canTransitionDisplay()) {
el.hidden = true;
} else {
el.setAttribute('data-closing', '');
}
const animations = el.getAnimations();
if (animations.length > 0) {
await Promise.race([
Promise.allSettled(animations.map((a) => a.finished)),
new Promise((r) => setTimeout(r, 2000)),
]);
}
// Skipped if showElement() cancelled the close mid-animation.
if (el.hasAttribute('data-closing')) {
el.removeAttribute('data-closing');
el.hidden = true;
}
}
function showElement(el) {
el.removeAttribute('data-closing');
el.hidden = false;
}
async function removeElement(el) {
await hideElement(el);
el.remove();
}