Các phần tử hiển thị trong "lớp trên cùng" (như <dialog>, các phần tử có thuộc tính popover hoặc chú giải công cụ) trước đây rất khó tạo ảnh động vì chúng chuyển đổi giữa display: none và trạng thái hiển thị. CSS hiện đại cung cấp @starting-style, transition-behavior: allow-discrete và thuộc tính overlay để cho phép chuyển đổi mượt mà khi vào và thoát cho các phần tử này. Xin lưu ý rằng các ví dụ dưới đây sử dụng tính năng lồng CSS gốc.
Triển khai
1. Bật hiệu ứng chuyển đổi riêng biệt
Để tạo ảnh động cho thuộc tính display, bạn phải đặt transition-behavior: allow-discrete. Điều này cho phép phần tử vẫn hiển thị trong quá trình chuyển đổi thoát. Nếu sử dụng cú pháp viết tắt cho hiệu ứng chuyển đổi, hãy nhớ đặt transition-behavior: allow-discrete sau đó để ngăn cú pháp viết tắt phủ định hiệu ứng chuyển đổi.
2. Tài sản overlay
Khi một phần tử di chuyển vào hoặc ra khỏi lớp trên cùng, phần tử đó phải chuyển đổi thuộc tính overlay. Điều này đảm bảo phần tử luôn ở lớp trên cùng trong suốt thời gian diễn ra ảnh động, ngăn phần tử bị các phần tử khác hoặc khung nhìn cắt bớt trước thời hạn.
3. Ảnh động xuất hiện bằng @starting-style
Sử dụng quy tắc at-rule @starting-style để xác định kiểu mà một phần tử sẽ chuyển đổi từ khi được kết xuất lần đầu hoặc khi display của phần tử đó thay đổi từ none.
4. Tạo ảnh động cho phông nền
Bạn có thể tạo ảnh động cho phần tử giả ::backdrop theo cách tương tự bằng cách áp dụng hiệu ứng chuyển đổi cho các thuộc tính của phần tử đó.
Ví dụ
/* 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;
}
}
}
Các quy tắc ràng buộc và hỗ trợ tiếp cận
- BẮT BUỘC: Thêm
overlayvào danh sáchtransitioncho mọi phần tử di chuyển vào hoặc ra khỏi lớp trên cùng. - BẮT BUỘC: Sử dụng
allow-discretecho quá trình chuyển đổi thuộc tínhdisplay. - BẮT BUỘC: Tôn trọng lựa chọn ưu tiên của người dùng về việc giảm chuyển động bằng cách sử dụng
prefers-reduced-motionđể đơn giản hoá hiệu ứng chuyển đổi (ví dụ: xoá hiệu ứng biến đổi và rút ngắn thời lượng). - NÊN: Đặt khối
@starting-stylebên trong hoặc sau bộ chọn trạng thái "mở" để đảm bảo hiệu ứng xếp tầng chính xác. - KHÔNG: Sử dụng
@starting-stylecho ảnh động thoát; ảnh động thoát được xác định bằng quá trình chuyển đổi sang trạng thái cơ sở (đóng).
Chiến lược dự phòng
Dự phòng và hỗ trợ trình duyệt cho transition-behavior
Firefox 129 trở lên phân tích cú pháp transition-behavior: allow-discrete (CSS.supports('transition-behavior', 'allow-discrete') trả về true) mà không thực sự chuyển đổi thuộc tính display (lỗi Firefox 1882408), khiến các phần tử biến mất ngay lập tức khi thoát.
Để phát hiện một cách đáng tin cậy tính năng hỗ trợ chuyển đổi display riêng biệt, hãy thăm dò xem display được tính toán của một phần tử tạm thời có hiển thị khi chuyển đổi sang none hay không, thay vì chỉ dựa vào 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;
}
Dự phòng thoát lớp trên cùng
Ảnh động khi xuất hiện hoạt động trong CSS thuần tuý trên tất cả trình duyệt hỗ trợ @starting-style. Bạn không cần lớp .is-opening vì hiệu ứng chuyển đổi khi xuất hiện không phụ thuộc vào hiệu ứng chuyển đổi overlay hoặc display riêng biệt.
Ảnh động thoát yêu cầu cả tính năng hỗ trợ chuyển đổi overlay và display riêng biệt. Khi một trong hai không được hỗ trợ (chẳng hạn như trong Firefox và Safari), hãy gói các bộ chọn trạng thái mở trong :is() và thêm :where(:not([data-closing])) (lồng &::backdrop và @starting-style) để chế độ cài đặt data-closing kích hoạt quá trình chuyển đổi thoát trong khi phần tử vẫn ở lớp trên cùng, sau đó đợi getAnimations() ổn định trước khi gọi .close() hoặc .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);
});
Không thể chặn thao tác đóng nhanh cửa sổ bật lên và nút bật/tắt popovertarget (chỉ có thể huỷ beforetoggle khi mở), vì vậy, trong các trình duyệt cần có phương án dự phòng, các lối thoát đó sẽ diễn ra ngay lập tức. Cung cấp một chế độ kiểm soát đóng rõ ràng để gọi closeTopLayer() nếu ảnh động thoát là quan trọng.
Phương án dự phòng và hỗ trợ trình duyệt cho Popover
Popover API chủ yếu là cải tiến tăng dần, nhưng các hành vi xác định của API này (quảng bá lớp trên cùng, đóng nhẹ và gọi popovertarget) không có hành vi tương đương chỉ dùng CSS. Các trình duyệt cũ cần có polyfill hoặc phương án dự phòng thủ công nếu bạn không muốn cung cấp polyfill.
Polyfill: Để hỗ trợ thuộc tính popover trong các trình duyệt cũ, hãy tải @oddbird/popover-polyfill theo điều kiện. BẮT BUỘC: Phát hiện tính năng bằng cách kiểm tra thuộc tính popover trên HTMLElement.prototype và chỉ tải polyfill khi thiếu tính năng hỗ trợ gốc — KHÔNG tải polyfill vô điều kiện.
Với trình đóng gói hoặc bản đồ nhập:
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
Nếu không có trình đóng gói, hãy nhập từ CDN bên trong <script type="module">:
<script type="module">
if (!("popover" in HTMLElement.prototype)) {
import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
}
</script>
Lưu ý về kiểu: Polyfill không thể xác định lớp giả :popover-open thực, vì vậy, lớp này sẽ áp dụng lớp .\:popover-open. BẮT BUỘC: Kết hợp cả hai bằng :is() hoặc :where(), nếu không, các trình duyệt thiếu :popover-open sẽ loại bỏ toàn bộ quy tắc:
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
Ngoài ra, đối với phương án dự phòng cũ không có polyfill, hãy sử dụng position: fixed và tính toán toạ độ theo cách thủ công thông qua getBoundingClientRect() hoặc dựa vào vị trí mặc định bằng inset: auto nếu điều đó phù hợp với trường hợp sử dụng.