12k
All articles

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.

OpenReplay Team
OpenReplay Team
Como Animar display: none Sem Hacks em JavaScript

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 display incluindo-o no shorthand transition com allow-discrete, e defina o estado de entrada em um bloco @starting-style — sem JavaScript.
  • display é uma propriedade discreta: com allow-discrete, o navegador alterna para visível em 0% na entrada e para none em 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-discrete de forma independente, ele deve vir depois do shorthand transition, 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, pois overlay ainda não é Baseline.
  • @starting-style e allow-discrete tornaram-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.

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.

RecursoStatusFallback
transition-behavior: allow-discreteBaseline (ago 2024)Exibição/ocultação instantânea
@starting-styleBaseline (ago 2024)Sem animação de entrada
overlayNão é BaselineDialog 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.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.