Animowanie elementów do i z warstwy najwyższego poziomu

Elementy renderowane w „górnej warstwie” (takie jak <dialog>, elementy z atrybutem popover lub etykietki) były dotychczas trudne do animowania, ponieważ przełączają się między stanem display: none a stanem widocznym. Nowoczesny CSS udostępnia @starting-style, transition-behavior: allow-discrete i właściwość overlay, aby umożliwić płynne przejścia wejścia i wyjścia dla tych elementów. Pamiętaj, że w poniższych przykładach użyto natywnego zagnieżdżania CSS.

Implementacja

1. Włącz przejścia dyskretne

Aby animować właściwość display, musisz ustawić transition-behavior: allow-discrete. Dzięki temu element pozostanie widoczny podczas przejścia wyjściowego. Jeśli używasz skróconych właściwości przejścia, pamiętaj, aby umieścić transition-behavior: allow-discrete po nich, aby skrócona właściwość nie unieważniła tej wartości.

2. Właściwość overlay

Gdy element jest przenoszony do górnej warstwy lub z niej, musi przejść przez właściwość overlay. Dzięki temu element pozostaje w górnej warstwie przez cały czas trwania animacji, co zapobiega jego przedwczesnemu przycięciu przez inne elementy lub widoczny obszar.

3. Animacje wejścia z użyciem @starting-style

Użyj reguły at-rule @starting-style, aby zdefiniować style, od których element powinien przejść, gdy jest renderowany po raz pierwszy lub gdy jego właściwość display zmienia się z none.

4. Animowanie tła

Pseudoelement ::backdrop można animować w podobny sposób, stosując przejścia do jego właściwości.

Przykład

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

Ograniczenia i ułatwienia dostępu

  • OBOWIĄZKOWO: dodaj overlay do listy transition dla każdego elementu, który jest przenoszony do warstwy górnej lub z niej.
  • OBOWIĄZKOWO: użyj allow-discrete do przejścia właściwości display.
  • OBOWIĄZKOWE: uwzględniaj preferencje użytkowników dotyczące ograniczenia ruchu za pomocą prefers-reduced-motion, upraszczając przejścia (np. usuwając przekształcenia i skracając czas trwania).
  • DO: umieść blok @starting-style wewnątrz selektora stanu „open” lub po nim, aby zapewnić prawidłowe kaskadowanie.
  • NIE: używaj @starting-style do animacji wyjścia. Animacje wyjścia są definiowane przez przejście do stanu podstawowego (zamkniętego).

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 zdarzenie wyjściowe w warstwie górnej

Animacje wejściowe działają w czystym CSS we wszystkich przeglądarkach obsługujących @starting-style – nie jest potrzebna klasa .is-opening, ponieważ przejścia wejściowe nie zależą od przejść overlay ani dyskretnych przejść display.

Animacje wyjścia wymagają obsługi przejść overlay i dyskretnych display. Jeśli któryś z nich nie jest obsługiwany (np. w przypadku przeglądarek Firefox i Safari), umieść selektory stanu otwartego w :is() i dodaj :where(:not([data-closing])) (zagnieżdżając &::backdrop i @starting-style), aby ustawienie data-closing aktywowało przejście wyjściowe, gdy element pozostaje w górnej warstwie, a następnie poczekaj na zakończenie getAnimations(), zanim wywołasz .close() lub .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);
});

Nie można przechwycić przełączników popovertarget i zamknięcia okienka wyskakującego (beforetoggle można anulować tylko podczas otwierania), więc w przeglądarkach, które wymagają kreacji zastępczej, te wyjścia są natychmiastowe. Jeśli animacja wyjścia ma znaczenie, udostępnij wyraźny element sterujący zamykaniem, który wywołuje funkcję closeTopLayer().

Kreacje zastępcze i obsługa wyskakujących okienek w przeglądarkach

Interfejs Popover API to głównie progresywne ulepszanie, ale jego definiujące zachowania – promowanie warstwy górnej, lekkie odrzucanie i wywoływanie popovertarget – nie mają odpowiednika tylko w CSS. Starsze przeglądarki wymagają polyfilla lub ręcznego rozwiązania zastępczego, jeśli nie chcesz go dostarczać.

Polyfill: aby obsługiwać atrybut popover w starszych przeglądarkach, warunkowo wczytuj @oddbird/popover-polyfill. OBOWIĄZKOWO: wykryj funkcję, sprawdzając właściwość popover w HTMLElement.prototype, i wczytaj polyfill tylko wtedy, gdy brakuje obsługi natywnej – NIE wczytuj go bezwarunkowo.

Za pomocą bundlera lub mapy importu:

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

Bez bundlera, importuj z sieci CDN w tagu <script type="module">:

<script type="module">
  if (!("popover" in HTMLElement.prototype)) {
    import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
  }
</script>

Uwaga dotycząca stylów: polyfill nie może zdefiniować prawdziwej pseudoklasy :popover-open, więc zamiast tego stosuje klasę .\:popover-open. OBOWIĄZKOWO: połącz te 2 właściwości za pomocą :is() lub :where(), w przeciwnym razie przeglądarki, które nie obsługują :popover-open, odrzucą całą regułę:

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

Możesz też użyć position: fixed i ręcznie obliczyć współrzędne za pomocą getBoundingClientRect() lub polegać na domyślnym pozycjonowaniu za pomocą inset: auto, jeśli jest to akceptowalne w danym przypadku użycia.