Anima l'entrata e l'uscita degli elementi

In passato, le transizioni CSS non potevano animare gli elementi quando venivano aggiunti per la prima volta al DOM o quando la loro proprietà display cambiava da none. La regola at-rule @starting-style e transition-behavior: allow-discrete forniscono un modo dichiarativo per creare animazioni di entrata e uscita fluide.

Implementazione

1. Animazione dei pulsanti di attivazione/disattivazione di display: none

Per animare un elemento quando ne attivi o disattivi la visibilità tramite un attributo (ad es. hidden con display: none):

  1. Definisci lo stato visibile: imposta i valori finali delle proprietà (ad es. opacity: 1) nella classe base.
  2. Definisci lo stato iniziale di entrata: utilizza @starting-style per specificare i valori di transizione da quando l'elemento diventa visibile.
  3. Attiva le transizioni discrete: includi display nella proprietà transition e utilizza transition-behavior: allow-discrete.
  4. Definisci lo stato di uscita: imposta i valori target nell'attributo 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. Animazione dell'inserimento e della rimozione del DOM

Per gli elementi aggiunti tramite appendChild() o rimossi tramite remove():

  • Voce: utilizza @starting-style come mostrato sopra. Il browser rileverà automaticamente la modifica dello stile da "nothing" agli stili iniziali dell'elemento e attiverà la transizione dai valori @starting-style.
  • Rimozione: poiché element.remove() è istantaneo e non attiva una transizione CSS da solo, devi prima attivare la transizione di uscita e attendere che termini prima di rimuovere il nodo dal 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();

Vincoli e accessibilità

  • OBBLIGATORIO: utilizza transition-behavior: allow-discrete durante la transizione display. Senza questa proprietà, l'elemento scompare istantaneamente durante l'uscita.
  • NON utilizzare allow-discrete nella sintassi abbreviata transition, altrimenti i browser meno recenti ignoreranno l'intera dichiarazione transition. Tranne nei casi d'uso in cui ciò è auspicabile, utilizza una dichiarazione transition-behavior: allow-discrete separata.
  • OBBLIGATORIO: utilizza @starting-style per le animazioni di entrata. I browser saltano le transizioni al primo aggiornamento dello stile di un elemento (rendering iniziale o modifica di display: none), a meno che non venga fornito questo valore.
  • DA FARE: includi overlay nell'elenco transition se animi elementi del livello superiore come <dialog> o popover per assicurarti che rimangano nel livello superiore durante l'animazione di uscita.
  • SÌ: rispetta le preferenze dell'utente per il movimento ridotto utilizzando la query supporti prefers-reduced-motion.
  • DA EVITARE: fare affidamento su @starting-style per le animazioni di uscita; definisce solo il punto di partenza per una transizione di entrata. Le animazioni di uscita sono definite dalla transizione allo stato nascosto.

Strategie di riserva

Fallback e supporto del browser per transition-behavior

Firefox 129+ analizza transition-behavior: allow-discrete (CSS.supports('transition-behavior', 'allow-discrete') restituisce true) senza eseguire effettivamente la transizione della proprietà display (bug di Firefox 1882408), causando la scomparsa immediata degli elementi all'uscita.

Per rilevare in modo affidabile il supporto della transizione display discreta, verifica se il valore display calcolato di un elemento temporaneo rimane visibile quando viene eseguita la transizione a none anziché fare affidamento esclusivamente su 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;
}

Uscita di riserva quando le transizioni display discrete non sono supportate

Le animazioni di entrata che utilizzano @starting-style funzionano su tutti i browser moderni senza JavaScript. Quando le transizioni display discrete non sono supportate (!canTransitionDisplay()), l'impostazione di hidden applica immediatamente display: none e salta l'animazione di uscita (sia quando si nasconde sia prima di element.remove()). Separa lo stato di uscita visivo ([data-closing]) da display: none ([hidden]) in modo che opacity e translate terminino l'animazione prima di nascondere o rimuovere l'elemento. Sostituisci le regole .card[hidden] del passaggio 1 (incluso l'override prefers-reduced-motion) con:

.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();
}