在「頂層」算繪的元素 (例如 <dialog>、具有 popover 屬性的元素或工具提示) 過去很難建立動畫,因為這些元素會在 display: none 和可見狀態之間切換。現代 CSS 提供 @starting-style、transition-behavior: allow-discrete 和 overlay 屬性,可為這些元素啟用流暢的進入和退出轉場效果。請注意,以下範例使用原生 CSS 巢狀結構。
導入作業
1. 啟用離散轉場效果
如要為 display 屬性建立動畫,您必須設定 transition-behavior: allow-discrete。這樣一來,元素在退出轉場期間仍會顯示。如果使用轉場效果簡寫,請務必在後面加上 transition-behavior: allow-discrete,以免簡寫將其否定。
2. overlay 屬性
當元素移入或移出頂層時,必須轉換 overlay 屬性。這可確保元素在動畫期間保持在頂層,避免過早遭到其他元素或可視區域裁剪。
3. 使用 @starting-style 製作進入動畫
使用 @starting-style at 規則,定義元素首次算繪或 display 從 none 變更時,應「從」哪個樣式轉換。
4. 為背景幕加入動畫
::backdrop 虛擬元素也可以透過類似方式,將轉場效果套用至其屬性,以建立動畫。
範例
/* 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;
}
}
}
限制與無障礙功能
- 必要:對於移入或移出頂層的任何元素,請在
transition清單中加入overlay。 - 必要:請使用
allow-discrete進行display屬性轉換。 - 強制:使用
prefers-reduced-motion簡化轉場效果 (例如移除轉換和縮短時間長度),尊重使用者減少動態效果的偏好設定。 - 建議做法:將
@starting-style區塊放在「open」狀態選取器內部或之後,確保正確串聯。 - 請勿:使用
@starting-style製作退出動畫;退出動畫是由轉換至基本 (關閉) 狀態定義。
備用策略
transition-behavior 的備用方案和瀏覽器支援
Firefox 129 以上版本會剖析 transition-behavior: allow-discrete (CSS.supports('transition-behavior', 'allow-discrete') 傳回 true),但不會實際轉換 display 屬性 (Firefox 錯誤 1882408),導致元素在退出時立即消失。
如要可靠地偵測離散 display 轉場支援,請探測暫時元素的計算 display 在轉換至 none 時是否仍可見,而不是僅依賴 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;
}
頂層離開事件備用廣告
在所有支援 @starting-style 的瀏覽器中,進入動畫都能以純 CSS 運作,不需要 .is-opening 類別,因為進入轉場效果不依賴 overlay 或離散 display 轉場效果。
退出動畫需要 overlay 和離散 display 轉場支援。如果瀏覽器不支援其中一項 (例如 Firefox 和 Safari),請將開啟狀態選取器包裝在 :is() 中,並附加 :where(:not([data-closing])) (巢狀結構 &::backdrop 和 @starting-style),這樣設定 data-closing 就會觸發退出轉場效果,同時元素仍會保留在頂層,然後等待 getAnimations() 穩定,再呼叫 .close() 或 .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);
});
彈出式視窗輕觸關閉和 popovertarget 切換鈕無法攔截 (beforetoggle 只有在開啟時才能取消),因此在需要備用方案的瀏覽器中,這些退出動作會立即執行。如果退出動畫很重要,請提供明確的關閉控制項,呼叫 closeTopLayer()。
Popover 的備用方案和瀏覽器支援
Popover API 主要是漸進增強,但其定義行為 (頂層升級、輕觸關閉和 popovertarget 叫用) 沒有僅限 CSS 的對等項目。舊版瀏覽器需要 polyfill,如果不想提供,則需要手動備用方案。
Polyfill:如要在舊版瀏覽器中支援 popover 屬性,請視情況載入 @oddbird/popover-polyfill。必要:檢查 HTMLElement.prototype 上的 popover 屬性,偵測功能,並僅在缺少原生支援時載入 polyfill,請勿無條件載入。
使用套件組合器或匯入對應:
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
如果沒有繫結器,請從 <script type="module"> 內的 CDN 匯入:
<script type="module">
if (!("popover" in HTMLElement.prototype)) {
import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
}
</script>
樣式注意事項:Polyfill 無法定義實際的 :popover-open 虛擬類別,因此會改為套用 .\:popover-open 類別。必要:請使用 :is() 或 :where() 結合兩者,否則不支援 :popover-open 的瀏覽器會捨棄整個規則:
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
或者,如要使用不含 polyfill 的舊版備用方案,請使用 position: fixed,並透過 getBoundingClientRect() 手動計算座標,或在用途允許的情況下,使用 inset: auto 預設定位。