Элементы, которые отображаются в верхнем слое (например, <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внутрь селектора состояния 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;
}
Резервный переход для верхнего слоя
Анимации входа работают на чистом CSS во всех браузерах, поддерживающих @starting-style. Класс .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. Для старых браузеров нужен полифил или резервный вариант, если вы не хотите его использовать.
Полифил. Чтобы обеспечить поддержку атрибута popover в старых браузерах, условно загрузите @oddbird/popover-polyfill. ОБЯЗАТЕЛЬНО. Проверьте наличие свойства popover в HTMLElement.prototype и загрузите полифил, только если встроенная поддержка отсутствует. Не загружайте его без необходимости.
С помощью сборщика или карты импорта:
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
Если вы не используете сборщик, импортируйте из 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>
Ограничение, связанное со стилями. Полифил не может определить настоящий псевдокласс :popover-open, поэтому вместо него применяется класс .\:popover-open. ОБЯЗАТЕЛЬНО. Объедините эти два правила с помощью :is() или :where(), иначе браузеры, не поддерживающие :popover-open, будут игнорировать правило целиком.
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
В качестве альтернативы для устаревшего резервного варианта без полифила используйте position: fixed и вручную рассчитайте координаты с помощью getBoundingClientRect() или используйте позиционирование по умолчанию с помощью inset: auto, если это допустимо для вашего случая.