Elementos que são renderizados na "camada superior" (como <dialog>, elementos com o atributo popover ou dicas) sempre foram difíceis de animar porque alternam entre display: none e um estado visível. O CSS moderno oferece @starting-style, transition-behavior: allow-discrete e a propriedade overlay para permitir transições de entrada e saída suaves para esses elementos. O aninhamento de CSS nativo é usado nos exemplos abaixo.
Implementação
1. Ativar transições discretas
Para animar a propriedade display, é necessário definir transition-behavior: allow-discrete. Isso permite que o elemento permaneça visível durante a transição de saída. Se você usar as abreviações de transição, coloque o transition-behavior: allow-discrete depois para evitar que a abreviação o anule.
2. A propriedade overlay
Quando um elemento entra ou sai da camada de cima, ele precisa fazer a transição da propriedade overlay. Isso garante que o elemento permaneça na camada superior durante a animação, evitando que ele seja cortado por outros elementos ou pela janela de visualização antes do tempo.
3. Animações de entrada com @starting-style
Use a regra @ @starting-style para definir os estilos de transição de um elemento a partir de quando ele é renderizado pela primeira vez ou quando o display muda de none.
4. Como animar o pano de fundo
O pseudoelemento ::backdrop pode ser animado de forma semelhante aplicando transições às próprias propriedades.
Exemplo
/* 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;
}
}
}
Restrições e acessibilidade
- OBRIGATÓRIO: inclua
overlayna listatransitionpara qualquer elemento que entre ou saia da camada superior. - OBRIGATÓRIO: use
allow-discretepara a transição da propriedadedisplay. - OBRIGATÓRIO: respeite as preferências do usuário para reduzir o movimento usando
prefers-reduced-motionao simplificar as transições (por exemplo, removendo transformações e reduzindo a duração). - FAÇA: coloque o bloco
@starting-styledentro ou depois do seletor de estado "open" para garantir a cascata adequada. - NÃO: use
@starting-stylepara animações de saída, que são definidas pela transição para o estado base (fechado).
Estratégias de fallback
Alternativas e suporte do navegador para transition-behavior
O Firefox 129+ analisa transition-behavior: allow-discrete (CSS.supports('transition-behavior', 'allow-discrete') retorna true) sem realmente fazer a transição da propriedade display (bug 1882408 do Firefox), fazendo com que os elementos desapareçam imediatamente ao sair.
Para detectar de forma confiável o suporte à transição discreta de display, verifique se o display calculado de um elemento temporário permanece visível quando é feita a transição para none, em vez de depender apenas de 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;
}
Substituto de saída da camada superior
As animações de entrada funcionam em CSS puro em todos os navegadores que oferecem suporte a @starting-style. Nenhuma classe .is-opening é necessária porque as transições de entrada não dependem de transições overlay ou display discretas.
As animações de saída exigem suporte à transição overlay e discreta display. Quando um deles não é compatível (como no Firefox e no Safari), envolva os seletores de estado aberto em :is() e anexe :where(:not([data-closing])) (aninhando &::backdrop e @starting-style) para que a configuração de data-closing acione a transição de saída enquanto o elemento permanece na camada superior. Em seguida, aguarde a definição de getAnimations() antes de chamar .close() ou .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);
});
A dispensa leve de popover e as alternâncias de popovertarget não podem ser interceptadas (beforetoggle só pode ser cancelado ao abrir). Portanto, em navegadores que precisam do substituto, essas saídas são instantâneas. Forneça um controle de fechamento explícito que chame closeTopLayer() se a animação de saída for importante.
Alternativas e suporte do navegador para Popover
A API Popover é, em grande parte, um aprimoramento progressivo, mas os comportamentos que a definem (promoção de camada superior, fechamento leve e invocação de popovertarget) não têm equivalente somente em CSS. Os navegadores mais antigos precisam de um polyfill ou de um substituto manual, se você preferir não enviar um.
Polyfill: para oferecer suporte ao atributo popover em navegadores mais antigos, carregue condicionalmente @oddbird/popover-polyfill. OBRIGATÓRIO: detecte o recurso verificando a propriedade popover em HTMLElement.prototype e carregue o polyfill somente quando não houver suporte nativo. NÃO o carregue incondicionalmente.
Com um bundler ou mapa de importação:
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
Sem um bundler, importe de uma CDN dentro de um <script type="module">:
<script type="module">
if (!("popover" in HTMLElement.prototype)) {
import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
}
</script>
Observação sobre o estilo: o polyfill não pode definir a pseudoclasse :popover-open real. Por isso, ele aplica uma classe .\:popover-open. OBRIGATÓRIO: combine os dois com :is() ou :where(). Caso contrário, os navegadores que não têm :popover-open vão descartar toda a regra:
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
Como alternativa, para um substituto legado sem um polyfill, use position: fixed e calcule manualmente as coordenadas usando getBoundingClientRect() ou use o posicionamento padrão com inset: auto, se isso for aceitável para o caso de uso.