Animar a entrada de um elemento que estava com display: none — como um modal, um popover ou um toast — sempre exigiu JavaScript: adicionar a classe, esperar um frame para o navegador aplicar o estado inicial, depois trocar para o estado final. A regra @starting-style resolve isso em CSS puro, definindo de onde uma transição deve partir no primeiro estilo aplicado ao elemento.
O problema: por que dá pra animar a saída, mas não a entrada
Uma transição normal de CSS precisa de dois valores: o estado atual e o estado novo. Isso funciona bem para animar a saída de um elemento, porque ele já está renderizado na tela quando o novo estilo é aplicado:
.aviso {
opacity: 1;
transition: opacity 0.3s;
}
.aviso.saindo {
opacity: 0;
}
Mas quando o elemento acabou de aparecer no DOM (ou saiu de display: none), o navegador não tem um "estado anterior" para animar a partir dele — o elemento simplesmente surge já no valor final, sem transição.
Como funciona o @starting-style
O bloco @starting-style define os valores que as propriedades devem assumir no instante em que o elemento recebe seu primeiro estilo — seja por ter acabado de entrar no DOM, seja por ter saído de display: none:
.aviso {
opacity: 1;
transition: opacity 0.3s;
}
@starting-style {
.aviso {
opacity: 0;
}
}
Com isso, assim que .aviso aparece, o navegador entende que deve começar a transição em opacity: 0 e animar até opacity: 1, exatamente como aconteceria numa transição comum de saída — mas ao contrário.
Animando display: none com transition-behavior
Só o @starting-style não resolve tudo, porque display é uma propriedade discreta: ela pula direto de um valor para o outro, sem estado intermediário. Para incluir display na transição, é preciso a propriedade transition-behavior: allow-discrete, que faz o navegador trocar o valor discreto só na metade do tempo da transição, em vez de no início:
.modal {
display: none;
opacity: 0;
transition: opacity 0.3s, display 0.3s allow-discrete;
}
.modal[open] {
display: block;
opacity: 1;
}
@starting-style {
.modal[open] {
opacity: 0;
}
}
O resultado é um fade-in completo: o elemento sai de display: none, começa em opacity: 0 por causa do @starting-style e transiciona suavemente até opacity: 1, tudo sem uma linha de JavaScript.
Exemplo prático: entrada e saída de um <dialog>
O caso mais comum é justamente o elemento <dialog>, que alterna entre exibido e removido do top layer:
<dialog id="modal">
<p>Conteúdo do modal</p>
<button onclick="modal.close()">Fechar</button>
</dialog>
<button onclick="modal.showModal()">Abrir modal</button>
dialog {
opacity: 0;
transform: scale(0.9);
transition: opacity 0.25s, transform 0.25s, display 0.25s allow-discrete, overlay 0.25s allow-discrete;
}
dialog[open] {
opacity: 1;
transform: scale(1);
}
@starting-style {
dialog[open] {
opacity: 0;
transform: scale(0.9);
}
}
Repare no overlay 0.25s allow-discrete: assim como display, a propriedade overlay (que controla a presença no top layer) também é discreta, e precisa do mesmo tratamento para que a transição de saída não seja cortada antes da hora.
@starting-style com múltiplos seletores
Assim como @media ou @supports, o @starting-style pode envolver vários seletores diferentes, e cada um deles define o estado inicial para o elemento correspondente:
@starting-style {
.toast {
opacity: 0;
translate: 0 20px;
}
.badge {
scale: 0;
}
}
Quando usar
- Animações de entrada para modais, popovers e toasts que usam
display: noneou o top layer. - Transições suaves para elementos condicionais renderizados por frameworks (React, Vue), substituindo bibliotecas de animação só para esse efeito.
- Fade-in de conteúdo recém-inserido no DOM via JavaScript, sem precisar forçar um reflow manual para "resetar" o estado inicial.
- Combinado com a Popover API e o
::backdrop, para animar tanto o conteúdo quanto o fundo escurecido.
Cuidados
O Firefox, mesmo já suportando @starting-style, tem restrições para animar a partir de display: none em alguns cenários, então vale testar o comportamento de saída com cuidado nesse navegador. Além disso, o @starting-style só tem efeito no primeiro estilo aplicado ao elemento — se as propriedades mudarem depois por outro motivo, a transição normal (sem @starting-style) é que entra em ação.
Compatibilidade
O @starting-style e o transition-behavior: allow-discrete atingiram Baseline "amplamente disponível" com o Firefox 129, em 2024, e já contam com suporte no Chrome, Edge e Safari. Para navegadores mais antigos, vale envolver o uso em @supports (transition-behavior: allow-discrete)) e manter um fallback sem animação de entrada.
Conclusão
O @starting-style fecha uma lacuna que existia há anos no CSS: animar a entrada de um elemento com a mesma naturalidade com que já se animava a saída. Combinado com transition-behavior: allow-discrete, elimina boa parte do JavaScript hoje usado só para coreografar o aparecimento de modais, popovers e outros elementos condicionais.
0 Comentário
Deixe seu Comentário!
Dúvida, sugestão ou só um "valeu!" — tudo é bem-vindo por aqui.