Animate Elements To and From Top Layer

Gli elementi che vengono visualizzati nel "livello superiore" (come <dialog>, gli elementi con l'attributo popover o i tooltip) sono storicamente difficili da animare perché passano da display: none a uno stato visibile. Il CSS moderno fornisce @starting-style, transition-behavior: allow-discrete e la proprietà overlay per consentire transizioni di entrata e uscita fluide per questi elementi. Tieni presente che negli esempi riportati di seguito viene utilizzato l'annidamento CSS nativo.

Implementazione

1. Attiva le transizioni discrete

Per animare la proprietà display, devi impostare transition-behavior: allow-discrete. In questo modo, l'elemento rimane visibile durante la transizione di uscita. Se utilizzi le proprietà abbreviate per le transizioni, assicurati di inserire transition-behavior: allow-discrete dopo per evitare che la proprietà abbreviata lo annulli.

2. La proprietà overlay

Quando un elemento entra o esce dal livello superiore, deve eseguire la transizione della proprietà overlay. In questo modo, l'elemento rimane nel livello superiore per tutta la durata dell'animazione, impedendo che venga ritagliato prematuramente da altri elementi o dalla finestra visibile.

3. Animazioni di ingresso con @starting-style

Utilizza la regola at-rule @starting-style per definire gli stili da cui un elemento deve eseguire la transizione quando viene visualizzato per la prima volta o quando il suo display cambia da none.

4. Animazione dello sfondo

Lo pseudo-elemento ::backdrop può essere animato in modo simile applicando transizioni alle sue proprietà.

Esempio

/* 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;
    }
  }
}

Vincoli e accessibilità

  • OBBLIGATORIO: includi overlay nell'elenco transition per qualsiasi elemento che entra o esce dal livello superiore.
  • OBBLIGATORIO: utilizza allow-discrete per la transizione della proprietà display.
  • OBBLIGATORIO: rispetta le preferenze dell'utente per il movimento ridotto utilizzando prefers-reduced-motion semplificando le transizioni (ad es. rimuovendo le trasformazioni e riducendo la durata).
  • SÌ: posiziona il blocco @starting-style all'interno o dopo il selettore di stato "open" per garantire una corretta applicazione a cascata.
  • NON: utilizzare @starting-style per le animazioni di uscita, che sono definite dalla transizione allo stato di base (chiuso).

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;
}

Procedimento di riserva per l'uscita dal livello superiore

Le animazioni di entrata funzionano in CSS puro su tutti i browser che supportano @starting-style: non è necessaria alcuna classe .is-opening perché le transizioni di entrata non dipendono da transizioni overlay o display discrete.

Le animazioni di uscita richiedono il supporto della transizione overlay e discreta display. Quando uno dei due non è supportato (ad esempio in Firefox e Safari), inserisci i selettori dello stato aperto in :is() e aggiungi :where(:not([data-closing])) (nidificando &::backdrop e @starting-style) in modo che l'impostazione di data-closing attivi la transizione di uscita mentre l'elemento rimane nel livello superiore, quindi attendi che getAnimations() si stabilizzi prima di chiamare .close() o .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 chiusura dei popover e i pulsanti di attivazione/disattivazione di popovertarget non possono essere intercettati (beforetoggle può essere annullato solo all'apertura), quindi nei browser che richiedono il fallback queste uscite sono istantanee. Fornisci un controllo di chiusura esplicito che chiami closeTopLayer() se l'animazione di uscita è importante.

Alternative e supporto del browser per Popover

L'API Popover è per lo più un potenziamento progressivo, ma i suoi comportamenti distintivi (promozione al livello superiore, chiusura con un clic e attivazione di popovertarget) non hanno un equivalente solo CSS. I browser meno recenti richiedono un polyfill o un fallback manuale se preferisci non fornirne uno.

Polyfill: per supportare l'attributo popover nei browser meno recenti, carica @oddbird/popover-polyfill in modo condizionale. OBBLIGATORIO: esegui il rilevamento delle funzionalità controllando la proprietà popover su HTMLElement.prototype e carica il polyfill solo quando manca il supporto nativo; NON caricarlo incondizionatamente.

Con un bundler o una mappa di importazione:

// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
  import("@oddbird/popover-polyfill");
}

Senza un bundler, importa da una CDN all'interno di 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>

Avvertenza sullo stile: il polyfill non può definire la pseudo-classe :popover-open reale, quindi applica una classe .\:popover-open. OBBLIGATORIO: combina i due elementi con :is() o :where(), altrimenti i browser che non dispongono di :popover-open ignorano l'intera regola:

[popover]:is(:popover-open, .\:popover-open) {
  display: block;
}

In alternativa, per un fallback legacy senza un polyfill, utilizza position: fixed e calcola manualmente le coordinate tramite getBoundingClientRect() o affidati al posizionamento predefinito con inset: auto se è accettabile per il caso d'uso.