Dominando @starting-style e Transições de Display
O problema que durou 15 anos
Animar um elemento de display: none para display: block sempre foi impossível em CSS puro. A propriedade display não é interpolável: ela muda de valor discretamente, num único frame. Quando o navegador encontra display: none, o elemento sai do layout e perde todo contexto de transição. Quando volta para display: block, ele aparece imediatamente, sem estado intermediário.
A solução historicamente envolvia JavaScript: adicionar uma classe, esperar o próximo frame com requestAnimationFrame (ou pior, setTimeout), e só então aplicar os estilos finais. Para a saída, era preciso escutar transitionend, e só depois setar display: none. Bibliotecas como Framer Motion, React Transition Group e GSAP existem em parte por causa dessa limitação.
Duas features CSS mudam isso: @starting-style e transition-behavior: allow-discrete. Juntas, elas permitem definir o estado inicial de um elemento que acabou de aparecer e transicionar propriedades discretas como display e overlay. O suporte já cobre Chrome 117+, Edge 117+ e Safari 17.5+. Firefox 129+ também implementa.
Como @starting-style funciona por baixo
Quando o navegador renderiza um elemento pela primeira vez (inserção no DOM ou mudança de display: none para qualquer valor visível), ele precisa de dois estados para interpolar: o estado "antes" e o estado "depois". Sem @starting-style, o "antes" não existe: o elemento simplesmente aparece com os estilos computados finais.
@starting-style define exatamente esse estado "antes". O motor de renderização lê os valores declarados dentro do bloco @starting-style, usa-os como ponto de partida, e transiciona até os valores computados normais do elemento.
/* O seletor dentro de @starting-style precisa ter
especificidade suficiente para casar com o elemento alvo */
.notification {
opacity: 1;
transform: translateY(0);
transition: opacity 300ms ease-out, transform 300ms ease-out;
@starting-style {
/* Estado "antes": de onde a transição parte quando o elemento aparece */
opacity: 0;
transform: translateY(-20px);
}
}
Esse bloco é avaliado apenas uma vez, no momento da primeira renderização do elemento. Depois que a transição completa, @starting-style é ignorado. Ele não afeta hover, focus ou qualquer outra mudança de estado posterior.
Transicionando display com allow-discrete
@starting-style sozinho resolve a animação de entrada para propriedades interpoláveis (opacity, transform). Para display, você precisa de mais uma peça: transition-behavior: allow-discrete.
Sem essa declaração, o navegador ignora display na lista de transições. Com ela, display participa da transição, mas de forma discreta: o valor muda no início da transição (entrada) ou no final (saída). Isso significa que, na entrada, display: block é aplicado imediatamente e as outras propriedades animam normalmente. Na saída, display: none só é aplicado quando as outras transições terminam.
.modal-overlay {
display: none;
opacity: 0;
transition:
display 400ms allow-discrete,
opacity 400ms ease-out;
@starting-style {
opacity: 0;
}
}
/* Quando o atributo open é adicionado (via JS ou <dialog>),
o elemento transiciona de opacity: 0 para opacity: 1.
display muda de none para block no primeiro frame. */
.modal-overlay.is-open {
display: block;
opacity: 1;
}
A sintaxe 400ms allow-discrete é um shorthand: transition-behavior: allow-discrete aplicado especificamente à propriedade display dentro da declaração transition. Você também pode declarar separadamente:
.modal-overlay {
transition: display 400ms, opacity 400ms ease-out;
transition-behavior: allow-discrete;
}
Animação de entrada e saída completa: o padrão que funciona
O cenário mais comum é um modal ou toast que precisa animar tanto na entrada quanto na saída. O padrão completo combina @starting-style (para a entrada), `allow-discrete
Leia o artigo completo em https://www.vivodecodigo.com.br/react/starting-style-transicoes-display-animacoes-entrada-saida-css