Menganimasikan Elemen Ke dan Dari Lapisan Atas

Elemen yang dirender di "lapisan atas" (seperti <dialog>, elemen dengan atribut popover, atau tooltip) secara historis sulit dianimasikan karena beralih antara display: none dan status terlihat. CSS modern menyediakan @starting-style, transition-behavior: allow-discrete, dan properti overlay untuk memungkinkan transisi masuk dan keluar yang lancar untuk elemen ini. Perhatikan bahwa penyusunan bertingkat CSS native digunakan dalam contoh di bawah.

Penerapan

1. Mengaktifkan Transisi Diskrit

Untuk menganimasikan properti display, Anda harus menetapkan transition-behavior: allow-discrete. Hal ini memungkinkan elemen tetap terlihat selama transisi keluarnya. Jika menggunakan singkatan transisi, pastikan untuk menempatkan transition-behavior: allow-discrete setelahnya untuk mencegah singkatan tersebut meniadakannya.

2. Properti overlay

Saat elemen berpindah masuk atau keluar dari lapisan atas, elemen tersebut harus mentransisikan properti overlay. Hal ini memastikan elemen tetap berada di lapisan atas selama durasi animasi, sehingga mencegahnya terpotong oleh elemen lain atau area pandang sebelum waktunya.

3. Animasi Masuk dengan @starting-style

Gunakan aturan-at @starting-style untuk menentukan gaya yang harus ditransisikan oleh elemen dari saat pertama kali dirender atau saat display-nya berubah dari none.

4. Menganimasikan Tampilan Latar

Elemen semu ::backdrop dapat dianimasikan dengan cara yang sama dengan menerapkan transisi ke propertinya sendiri.

Contoh

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

Batasan & Aksesibilitas

  • WAJIB: Sertakan overlay dalam daftar transition untuk elemen apa pun yang berpindah ke atau keluar dari lapisan atas.
  • WAJIB: Gunakan allow-discrete untuk transisi properti display.
  • WAJIB: Hormati preferensi pengguna untuk mengurangi gerakan menggunakan prefers-reduced-motion dengan menyederhanakan transisi (misalnya, menghapus transformasi dan mempersingkat durasi).
  • LAKUKAN: Tempatkan blok @starting-style di dalam atau setelah pemilih status "open" untuk memastikan pengurutan yang tepat.
  • JANGAN: Gunakan @starting-style untuk animasi keluar; animasi keluar ditentukan oleh transisi ke status dasar (tertutup).

Strategi penggantian

Penggantian & dukungan browser untuk transition-behavior

Firefox 129+ mengurai transition-behavior: allow-discrete (CSS.supports('transition-behavior', 'allow-discrete') menampilkan true) tanpa benar-benar mentransisikan properti display (bug Firefox 1882408), sehingga menyebabkan elemen langsung menghilang saat keluar.

Untuk mendeteksi dukungan transisi display diskret dengan andal, periksa apakah display yang dihitung dari elemen sementara tetap terlihat saat ditransisikan ke none, bukan hanya mengandalkan 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;
}

Penggantian keluar lapisan teratas

Animasi entri berfungsi di CSS murni di semua browser yang mendukung @starting-style—tidak diperlukan class .is-opening karena transisi entri tidak bergantung pada transisi overlay atau display diskret.

Animasi keluar memerlukan dukungan transisi overlay dan display diskret. Jika salah satunya tidak didukung (seperti di Firefox dan Safari), gabungkan pemilih status terbuka di :is() dan tambahkan :where(:not([data-closing])) (menyusun bertingkat &::backdrop dan @starting-style) sehingga setelan data-closing memicu transisi keluar saat elemen tetap berada di lapisan atas, lalu tunggu hingga getAnimations() selesai sebelum memanggil .close() atau .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);
});

Penutupan ringan popover dan tombol popovertarget tidak dapat dicegat (beforetoggle hanya dapat dibatalkan saat dibuka), sehingga di browser yang memerlukan penggantian, keluar tersebut bersifat instan. Berikan kontrol tutup eksplisit yang memanggil closeTopLayer() jika animasi keluar penting.

Penggantian & dukungan browser untuk Popover

Popover API sebagian besar merupakan progressive enhancement, tetapi perilaku penentuannya — promosi lapisan atas, penutupan ringan, dan pemanggilan popovertarget — tidak memiliki padanan khusus CSS. Browser lama memerlukan polyfill, atau penggantian manual jika Anda tidak ingin mengirimkannya.

Polyfill: Untuk mendukung atribut popover di browser lama, muat @oddbird/popover-polyfill secara bersyarat. WAJIB: Deteksi fitur dengan memeriksa properti popover di HTMLElement.prototype, dan muat polyfill hanya jika dukungan native tidak ada — JANGAN memuatnya tanpa syarat.

Dengan bundler atau peta impor:

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

Tanpa bundler, impor dari CDN di dalam <script type="module">:

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

Peringatan gaya: Polyfill tidak dapat menentukan pseudo-class :popover-open yang sebenarnya, sehingga menerapkan class .\:popover-open. WAJIB: Gabungkan keduanya dengan :is() atau :where(). Jika tidak, browser yang tidak memiliki :popover-open akan menghapus seluruh aturan:

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

Atau, untuk penggantian lama tanpa polyfill, gunakan position: fixed dan hitung koordinat secara manual melalui getBoundingClientRect() atau andalkan pemosisian default dengan inset: auto jika hal itu dapat diterima untuk kasus penggunaan.