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):
- Definisci lo stato visibile: imposta i valori finali delle proprietà (ad es.
opacity: 1) nella classe base. - Definisci lo stato iniziale di entrata: utilizza
@starting-styleper specificare i valori di transizione da quando l'elemento diventa visibile. - Attiva le transizioni discrete: includi
displaynella proprietàtransitione utilizzatransition-behavior: allow-discrete. - 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-stylecome 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-discretedurante la transizionedisplay. Senza questa proprietà, l'elemento scompare istantaneamente durante l'uscita. - NON utilizzare
allow-discretenella sintassi abbreviatatransition, altrimenti i browser meno recenti ignoreranno l'intera dichiarazionetransition. Tranne nei casi d'uso in cui ciò è auspicabile, utilizza una dichiarazionetransition-behavior: allow-discreteseparata. - OBBLIGATORIO: utilizza
@starting-styleper le animazioni di entrata. I browser saltano le transizioni al primo aggiornamento dello stile di un elemento (rendering iniziale o modifica didisplay: none), a meno che non venga fornito questo valore. - DA FARE: includi
overlaynell'elencotransitionse animi elementi del livello superiore come<dialog>opopoverper 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-styleper 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();
}