Animer l'entrée et la sortie des éléments

Fonctionnalités Web : @starting-style transition-behavior

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) :

  1. Définir l'état visible : définissez les valeurs de propriété finales (par exemple, opacity: 1) sur la classe de base.
  2. Définir l'état de départ de l'entrée : utilisez @starting-style pour spécifier les valeurs à partir desquelles effectuer la transition lorsque l'élément devient visible.
  3. Activer les transitions discrètes : incluez display dans la propriété transition et utilisez transition-behavior: allow-discrete.
  4. 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-style comme 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-discrete lors de la transition de display. Sans cela, l'élément disparaîtra instantanément lors de la sortie.
  • N'UTILISEZ PAS allow-discrete dans la notation abrégée transition, car les anciens navigateurs ignoreront l'intégralité de la déclaration transition. Sauf dans les cas d'utilisation où cela est souhaitable, utilisez une déclaration transition-behavior: allow-discrete distincte.
  • OBLIGATOIRE : Utilisez @starting-style pour 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 de display: none), sauf si cette propriété est fournie.
  • À FAIRE : Incluez overlay dans la liste transition si vous animez des éléments de la couche supérieure tels que <dialog> ou popover pour 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-style pour 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();
}