W przeszłości przejścia CSS nie mogły animować elementów, gdy były one po raz pierwszy dodawane do DOM lub gdy ich właściwość display zmieniała się z none. Reguła at-rule @starting-style i transition-behavior: allow-discrete umożliwiają deklaratywne tworzenie płynnych animacji wejścia i wyjścia.
Implementacja
1. Animowanie przełączników display: none
Aby animować element, gdy jego widoczność jest przełączana za pomocą atrybutu (np. hidden z display: none):
- Zdefiniuj stan widoczności: ustaw ostateczne wartości właściwości (np.
opacity: 1) w klasie podstawowej. - Zdefiniuj stan początkowy wejścia: użyj
@starting-style, aby określić wartości, od których ma się zaczynać przejście, gdy element stanie się widoczny. - Włącz przejścia dyskretne: we właściwości
transitionuwzględnijdisplayi użyjtransition-behavior: allow-discrete. - Zdefiniuj stan wyjściowy: ustaw wartości docelowe w atrybucie
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. Animowanie wstawiania i usuwania elementów DOM
W przypadku elementów dodanych za pomocą appendChild() lub usuniętych za pomocą remove():
- Wpis: użyj
@starting-style, jak pokazano powyżej. Przeglądarka automatycznie wykryje zmianę stylu z „nic” na początkowe style elementu i wywoła przejście z wartości@starting-style. - Usuwanie: ponieważ
element.remove()jest natychmiastowe i nie wywołuje samodzielnie przejścia CSS, musisz najpierw wywołać przejście wyjściowe i poczekać, aż się zakończy, zanim usuniesz węzeł z 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();
Ograniczenia i ułatwienia dostępu
- OBOWIĄZKOWO: użyj
transition-behavior: allow-discretepodczas przejściadisplay. Bez niego element natychmiast zniknie podczas wyjścia. - NIE używaj
allow-discretew skrócietransition– spowoduje to, że starsze przeglądarki będą ignorować całą deklaracjętransition. Z wyjątkiem przypadków, w których jest to pożądane, używaj oddzielnej deklaracjitransition-behavior: allow-discrete. - OBOWIĄZKOWO: w przypadku animacji wejścia używaj
@starting-style. Przeglądarki pomijają przejścia przy pierwszej aktualizacji stylu elementu (początkowe renderowanie lub zmianadisplay: none), chyba że podano tę wartość. - TAK: uwzględnij
overlayna liścietransition, jeśli animujesz elementy najwyższej warstwy, takie jak<dialog>lubpopover, aby mieć pewność, że pozostaną w najwyższej warstwie podczas animacji zakończenia. - DO: respektuj preferencje użytkowników dotyczące ograniczenia ruchu, używając zapytania o media
prefers-reduced-motion. - NIE: polegaj na
@starting-stylew przypadku animacji wyjścia – definiuje on tylko punkt początkowy przejścia wejściowego. Animacje wyjścia są definiowane przez przejście do stanu ukrytego.
Strategie zastępcze
Kreacje zastępcze i obsługa właściwości transition-behavior w przeglądarkach
Firefox 129 i nowsze wersje analizują transition-behavior: allow-discrete (CSS.supports('transition-behavior', 'allow-discrete') zwraca true) bez faktycznego przejścia właściwości display (błąd Firefoksa 1882408), co powoduje, że elementy natychmiast znikają po wyjściu.
Aby niezawodnie wykrywać obsługę dyskretnych przejść display, sprawdź, czy obliczona wartość display elementu tymczasowego pozostaje widoczna po przejściu do none, zamiast polegać wyłącznie na 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;
}
Zastępcze wyjście, gdy przejścia dyskretne display nie są obsługiwane
Animacje wejściowe wykorzystujące @starting-style działają we wszystkich nowoczesnych przeglądarkach bez JavaScriptu. Gdy dyskretne przejścia display nie są obsługiwane (!canTransitionDisplay()), ustawienie hidden powoduje natychmiastowe zastosowanie display: none i pominięcie animacji wyjścia (zarówno podczas ukrywania, jak i przed element.remove()). Oddziel wizualny stan wyjścia ([data-closing]) od display: none ([hidden]), aby opacity i translate zakończyły animację przed ukryciem lub usunięciem elementu. Zastąp reguły .card[hidden] z kroku 1 (w tym zastąpienie prefers-reduced-motion) tymi regułami:
.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();
}