Como Animar display: none Sem Hacks em JavaScript
Anime display none com CSS usando allow-discrete e @starting-style, além de overlay para dialog e popover e notas de suporte do navegador.
Para animar um elemento de ou para display: none apenas com CSS, adicione display à sua transition com a palavra-chave allow-discrete e forneça o estado de entrada em uma regra @starting-style — sem JavaScript, sem setTimeout, sem listener de transitionend. Dois recursos foram lançados para tornar isso possível: transition-behavior: allow-discrete permite que propriedades discretas como display participem de uma transição, e @starting-style fornece ao navegador um estado “antes de abrir” para animar a partir dele. Este guia apresenta receitas prontas para copiar e colar de entrada e saída, o caso de dialog/popover na camada superior com sua armadilha do overlay, e uma análise honesta sobre o suporte atual dos navegadores.
Principais Conclusões
- Anime
displayincluindo-o no shorthandtransitioncomallow-discrete, e defina o estado de entrada em um bloco@starting-style— sem JavaScript. displayé uma propriedade discreta: comallow-discrete, o navegador alterna para visível em 0% na entrada e paranoneem 100% na saída, de modo que o conteúdo permanece visível durante toda a duração.- Se você usar o
transition-behavior: allow-discretede forma independente, ele deve vir depois do shorthandtransition, caso contrário o navegador o ignorará. - Para dialogs e popovers, faça também a transição de
overlay ... allow-discrete, mas trate isso como progressive enhancement, poisoverlayainda não é Baseline. @starting-styleeallow-discretetornaram-se Baseline Newly available desde o lançamento do Firefox 129 em 6 de agosto de 2024; sem suporte, o elemento simplesmente aparece e desaparece instantaneamente.
Por que display: none não podia ser transicionado
Uma transição CSS precisa de um estilo anterior à mudança para animar a partir dele. Um elemento com display: none — ou que acabou de ser inserido no DOM — não tem estado renderizado anterior, portanto, historicamente o navegador não tinha nada para interpolar e a transição nunca era disparada. É por isso que os desenvolvedores recorriam a hacks de alternância de classes com setTimeout ou duplo requestAnimationFrame para forçar o reflow.
O atalho comum — aplicar fade com opacity: 0 e deixar o elemento no DOM — não é um substituto adequado. opacity: 0 mantém o elemento no layout, na ordem de tabulação e na árvore de acessibilidade, de modo que usuários de teclado e leitores de tela ainda podem acessar conteúdo que parece ter desaparecido. display: none o remove dos três. Na prática, gravações de sessão de componentes parcialmente migrados revelam exatamente essa classe de bug: um usuário navegando por tabulação ou clicando em um painel que parece fechado, mas foi apenas esmaecido e nunca removido do layout. Animar o display: none real elimina esse problema.
Discover how at OpenReplay.com.
Os dois recursos CSS que resolvem o problema: allow-discrete e @starting-style
display é uma propriedade discreta — ela não consegue interpolar entre valores, é um interruptor liga/desliga. Propriedades animadas de forma discreta geralmente alternam entre dois valores a 50% da animação; a exceção ocorre ao animar de ou para display: none ou content-visibility: hidden, caso em que o navegador alterna entre os dois valores de forma que o conteúdo em transição seja exibido durante toda a duração da animação. A direção importa: ao animar display de none para block, o valor muda para block em 0% da duração, ficando visível o tempo todo; ao animar de block para none, muda para none em 100% da duração, permanecendo visível o tempo todo — o que é o que mantém o fade realmente visível nas duas direções.
Você habilita isso definindo transition-behavior: allow-discrete na transição de display, e fornece o estado de entrada com @starting-style.
A receita de saída: transição para display: none
Para aplicar fade em um elemento e depois removê-lo do layout, inclua display na transition com allow-discrete e defina o estado oculto:
.panel {
opacity: 1;
transition: opacity 0.3s ease, display 0.3s allow-discrete;
}
.panel.is-hidden {
opacity: 0;
display: none;
}
A opacidade anima para 0 em 300ms; display permanece no valor visível até 100%, depois muda para none. Esquecer allow-discrete em display faz o elemento desaparecer instantaneamente — o erro mais comum com essa técnica.
A receita de entrada: animando a partir de display: none
Para um elemento que começa oculto, coloque os valores “antes de abrir” em uma regra @starting-style. Com CSS nesting, tudo fica em um único bloco:
.panel {
display: none;
opacity: 0;
transition: opacity 0.3s ease, display 0.3s allow-discrete;
}
.panel.is-open {
display: block;
opacity: 1;
@starting-style {
opacity: 0;
}
}
A ordem importa. @starting-style tem a mesma especificidade que a regra que ele alvo, portanto deve vir após a declaração do estado aberto para prevalecer na cascata. E há uma armadilha de uma linha: se você aplicar transition-behavior: allow-discrete antes do shorthand de transition, o navegador ignorará o transition-behavior. Quando escrito de forma independente, ele deve vir por último:
.panel {
transition: opacity 0.3s, display 0.3s;
transition-behavior: allow-discrete; /* deve vir DEPOIS do shorthand */
}
Para engines mais antigas, o padrão cross-browser do MDN declara transition duas vezes — a primeira instância sem allow-discrete fornece suporte cross-browser, garantindo que as outras propriedades ainda façam transição em navegadores que não suportam transition-behavior.
Dialogs e popovers: a armadilha do overlay
Elementos na camada superior — <dialog> e qualquer elemento usando o atributo popover — são o caso de uso de maior valor, e possuem um requisito adicional. Para dialogs e popovers, você também deve fazer a transição de overlay ... allow-discrete, caso contrário o elemento sai da camada superior instantaneamente e a animação de saída nunca é exibida:
dialog {
translate: 0 100vh;
transition:
translate 0.4s ease-out,
display 0.4s allow-discrete,
overlay 0.4s allow-discrete;
}
dialog[open] {
translate: 0 0;
@starting-style {
translate: 0 100vh;
}
}
Para um popover, substitua dialog[open] pela pseudo-classe :popover-open. A propriedade overlay é o que adia a saída da camada superior: ela garante que a remoção do elemento da camada superior seja adiada até que a animação seja concluída; em casos mais complexos, não fazer isso pode resultar na remoção prematura do elemento do overlay, tornando a animação irregular ou ineficaz.
Uma ressalva honesta que os concorrentes ignoram: overlay não é Baseline. O MDN o marca como experimental — este recurso não é Baseline porque não funciona em alguns dos navegadores mais amplamente utilizados. Trate-o como progressive enhancement; onde não for suportado, o dialog ainda abre e fecha, apenas sem a saída adiada.
Suporte dos navegadores e degradação graciosa
@starting-style e transition-behavior: allow-discrete tornaram-se Baseline Newly available com o Firefox 129, lançado em 6 de agosto de 2024. Mas animar o próprio display requer mais do que esses dois recursos. Funciona no Chrome e Edge 117+ e no Safari 18+ — o Safari 17.4 adicionou transition-behavior e o 17.5 adicionou @starting-style, mas a transição de display com eles só funciona a partir do Safari 18. O Firefox 129+ suporta ambos os recursos, porém até meados de 2026 ainda não faz a transição da propriedade display, portanto no Firefox o elemento simplesmente aparece e desaparece instantaneamente. O uso de display em @keyframes funciona desde o Chrome 116.
| Recurso | Status | Fallback |
|---|---|---|
transition-behavior: allow-discrete | Baseline (ago 2024) | Exibição/ocultação instantânea |
@starting-style | Baseline (ago 2024) | Sem animação de entrada |
overlay | Não é Baseline | Dialog ainda abre/fecha |
Isso é progressive enhancement puro. Sem esses recursos, elementos que animam para a camada superior ou a partir de um estilo display: none simplesmente aparecerão na página sem a transição, como acontece hoje. Sem polyfill, sem fallback em JavaScript. Use detecção de recursos para estabelecer um limite explícito, se desejar:
@supports (transition-behavior: allow-discrete) {
/* animações modernas de entrada/saída */
}
Quando usar View Transitions em vez disso
Use essas transições quando estiver alternando a visibilidade de um elemento existente. Recorra à View Transitions API quando estiver adicionando ou removendo nós do DOM. As view transitions no mesmo documento tornaram-se Baseline Newly available em 14 de outubro de 2025, após o lançamento do Firefox 144 na mesma data — com suporte no Chrome 111+, Edge 111+, Safari 18+ e Firefox 144+. Envolva a mutação do DOM em document.startViewTransition() com um fallback simples:
if (document.startViewTransition) {
document.startViewTransition(() => card.remove());
} else {
card.remove();
}
A receita moderna aposenta a antiga orquestração em JavaScript: inclua display na sua transition com allow-discrete, defina o estado de entrada em @starting-style, adicione overlay para elementos na camada superior, e deixe os navegadores sem suporte recorrerem à troca instantânea. Abandone o setTimeout e entregue o CSS.
Perguntas Frequentes
Por que minha animação de saída não dispara mesmo após adicionar allow-discrete à transição de display?
A causa mais comum é que a declaração independente transition-behavior: allow-discrete vem antes do shorthand transition, fazendo com que o navegador a ignore silenciosamente. Quando escrito como uma propriedade separada, transition-behavior deve aparecer após o shorthand transition, caso contrário o shorthand o redefine. Se você incluir allow-discrete diretamente no valor de transition, a ordem dentro do shorthand não importa e essa armadilha não se aplica.
Ainda preciso de @starting-style se quero apenas aplicar fade em um elemento para display: none?
Não. @starting-style só é necessário para animações de entrada, quando o elemento parte de display: none ou é recém-inserido no DOM e precisa de um estado anterior para animar a partir dele. Uma saída pura — animando um elemento já visível para display: none — requer apenas que display seja incluído na transition com allow-discrete e o estado oculto definido. Adicione @starting-style somente quando também quiser animar a entrada do elemento.
O que acontece em navegadores que não suportam transition-behavior ou @starting-style?
O elemento simplesmente aparece e desaparece instantaneamente, exatamente como aconteceria sem nenhuma animação. Isso é progressive enhancement, portanto não são necessários polyfill nem fallback em JavaScript. Para dialogs e popovers, navegadores que não suportam a propriedade overlay não-Baseline ainda abrem e fecham o elemento corretamente; eles apenas ignoram a saída adiada da camada superior. Você pode delimitar o enhancement explicitamente com uma regra @supports (transition-behavior: allow-discrete).
Quando devo usar a View Transitions API em vez de animar display: none?
Use View Transitions quando estiver adicionando ou removendo nós do DOM, e use transições de display quando estiver alternando a visibilidade de um elemento que já existe no DOM. As view transitions no mesmo documento tornaram-se Baseline Newly available em 14 de outubro de 2025, com suporte no Chrome 111+, Edge 111+, Safari 18+ e Firefox 144+. Envolva a mutação do DOM em document.startViewTransition() com um fallback simples para engines sem suporte.