องค์ประกอบที่แสดงผลใน "เลเยอร์บนสุด" (เช่น <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 เพื่อกำหนดสไตล์ที่องค์ประกอบควรเปลี่ยนจากเมื่อแสดงผลครั้งแรกหรือเมื่อ 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;
}
}
}
ข้อจำกัดและการช่วยเหลือพิเศษ
- จำเป็น: ใส่
overlayในรายการtransitionสำหรับองค์ประกอบที่ย้ายเข้าหรือออกจากเลเยอร์บนสุด - บังคับ: ใช้
allow-discreteสำหรับการเปลี่ยนพร็อพเพอร์ตี้display - บังคับ: เคารพค่ากำหนดของผู้ใช้สำหรับการลดการเคลื่อนไหวโดยใช้
prefers-reduced-motionด้วยการลดความซับซ้อนของการเปลี่ยน (เช่น นำการเปลี่ยนรูปแบบออกและลดระยะเวลา) - ควรทำ: วางบล็อก
@starting-styleไว้ภายในหรือหลังตัวเลือกสถานะ "เปิด" เพื่อให้แน่ใจว่ามีการเรียงซ้อนที่เหมาะสม - อย่า: ใช้
@starting-styleสำหรับภาพเคลื่อนไหวเมื่อออก เนื่องจากภาพเคลื่อนไหวเมื่อออกจะกำหนดโดยการเปลี่ยนสถานะเป็นสถานะพื้นฐาน (ปิด)
กลยุทธ์สำรอง
การสนับสนุนเบราว์เซอร์และทางเลือกสำรองสำหรับ transition-behavior
Firefox 129 ขึ้นไปจะแยกวิเคราะห์ transition-behavior: allow-discrete (CSS.supports('transition-behavior', 'allow-discrete') แสดงผล true) โดยไม่เปลี่ยนผ่านพร็อพเพอร์ตี้ display จริงๆ (ข้อบกพร่อง 1882408 ของ Firefox) ซึ่งทำให้องค์ประกอบหายไปทันทีเมื่อออก
หากต้องการตรวจจับการรองรับการเปลี่ยน 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;
}
โฆษณาสำรองเมื่อออกจากเลเยอร์บน
ภาพเคลื่อนไหวขาเข้าทำงานใน CSS แบบล้วนๆ ในเบราว์เซอร์ทั้งหมดที่รองรับ @starting-style โดยไม่จำเป็นต้องใช้คลาส .is-opening เนื่องจากทรานซิชันขาเข้าไม่ได้ขึ้นอยู่กับทรานซิชัน overlay หรือ display แบบแยกกัน
ภาพเคลื่อนไหวเมื่อออกต้องรองรับทั้งการเปลี่ยน overlay และ display แบบแยกกัน เมื่อไม่รองรับทั้ง 2 อย่าง (เช่น ใน 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);
});
ไม่สามารถขัดจังหวะการปิด Popover แบบ Light Dismiss และการสลับ popovertarget (beforetoggle จะยกเลิกได้เมื่อเปิดเท่านั้น) ดังนั้นในเบราว์เซอร์ที่ต้องใช้การสำรอง การออกจากระบบเหล่านั้นจะเกิดขึ้นทันที ระบุตัวควบคุมการปิดที่ชัดเจนซึ่งเรียกใช้ closeTopLayer() หากภาพเคลื่อนไหวทางออกมีความสำคัญ
การรองรับเบราว์เซอร์และทางเลือกสำรองสำหรับ Popover
Popover API ส่วนใหญ่เป็นการเพิ่มประสิทธิภาพแบบต่อเนื่อง แต่ลักษณะการทำงานที่กำหนดไว้ เช่น การโปรโมตเลเยอร์บนสุด การปิดเมื่อคลิกนอกองค์ประกอบ และการเรียกใช้ popovertarget ไม่มี CSS ที่เทียบเท่า เบราว์เซอร์รุ่นเก่าต้องใช้ Polyfill หรือการสำรองข้อมูลด้วยตนเองหากคุณไม่ต้องการจัดส่ง
Polyfill: หากต้องการรองรับแอตทริบิวต์ popover ในเบราว์เซอร์รุ่นเก่า ให้โหลด @oddbird/popover-polyfill แบบมีเงื่อนไข จำเป็น: ตรวจหาฟีเจอร์โดยการตรวจสอบพร็อพเพอร์ตี้ popover ใน HTMLElement.prototype และโหลด Polyfill เฉพาะเมื่อไม่มีการรองรับแบบเนทีฟ อย่าโหลดโดยไม่มีเงื่อนไข
เมื่อใช้ Bundler หรือแผนที่การนำเข้า ให้ทำดังนี้
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
หากไม่มี Bundler ให้นำเข้าจาก CDN ภายใน <script type="module">:
<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 แทน จำเป็น: รวมทั้ง 2 รายการเข้าด้วยกันโดยใช้ :is() หรือ :where() ไม่เช่นนั้นเบราว์เซอร์ที่ไม่มี :popover-open จะทิ้งกฎทั้งหมด
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
หรือสำหรับทางเลือกสำรองแบบเดิมที่ไม่มี Polyfill ให้ใช้ position: fixed และคำนวณพิกัดด้วยตนเองผ่าน getBoundingClientRect() หรือใช้การกำหนดตำแหน่งเริ่มต้นด้วย inset: auto หากยอมรับได้สำหรับกรณีการใช้งาน