Você já escreveu algo assim?
// Adiciona o elemento sem a classe de animação
element.classList.remove('visible');
document.body.appendChild(element);
// Espera um frame para o browser "ver" o estado inicial
setTimeout(() => {
element.classList.add('visible');
}, 10);
Esse setTimeout(fn, 10) é um hack. Você está esperando o browser renderizar um frame com o elemento invisível para então ativar a transição de entrada. Funciona, mas é frágil e semanticamente errado.
@starting-style resolve isso em CSS.
Por que transições não funcionam na entrada
As transições CSS animam entre dois estados. Quando um elemento aparece no DOM, não há estado anterior — ele simplesmente existe. O browser não tem de onde partir para criar a animação.
@starting-style define exatamente esse estado de partida. Você declara como o elemento deve parecer antes da primeira renderização, e o browser usa isso como ponto de início da transição.
Sintaxe
.notificacao {
opacity: 1;
transform: translateY(0);
transition: opacity .3s, transform .3s;
}
@starting-style {
.notificacao {
opacity: 0;
transform: translateY(-12px);
}
}
Quando .notificacao entra no DOM (ou quando display muda de none para qualquer outro valor), o browser lê o @starting-style como estado inicial e anima até o estado normal definido no seletor principal.
Zero JavaScript para a animação de entrada.
Animando a saída também
Para animar a saída com CSS puro, combine com a transição de display usando allow-discrete:
.notificacao {
opacity: 1;
transform: translateY(0);
transition:
opacity .3s,
transform .3s,
display .3s allow-discrete; /* espera a transição terminar antes de display:none */
}
.notificacao.saindo {
opacity: 0;
transform: translateY(-12px);
display: none;
}
@starting-style {
.notificacao {
opacity: 0;
transform: translateY(-12px);
}
}
Agora a entrada e a saída são animadas. O JavaScript só precisa adicionar/remover classes ou o elemento do DOM — nenhuma lógica de timing.
Com Popover e Dialog
@starting-style funciona nativamente com a Popover API e com <dialog>:
[popover] {
opacity: 1;
transform: scale(1);
transition: opacity .2s, transform .2s,
display .2s allow-discrete,
overlay .2s allow-discrete;
}
/* Estado de saída */
[popover]:not(:popover-open) {
opacity: 0;
transform: scale(.95);
}
/* Estado de entrada */
@starting-style {
[popover]:popover-open {
opacity: 0;
transform: scale(.95);
}
}
A propriedade overlay controla quando o elemento sai do top layer — sem ela na transição, o popover seria removido da camada imediatamente, interrompendo a animação de saída.
Casos de uso
- Toasts e notificações — entram e saem com slide suave
- Modais e popovers — fade + scale sem uma linha de JS
- Itens de lista dinâmica — animar cada item adicionado à lista
- Estados de loading — skeleton que fade-in quando o conteúdo chega
Aninhamento com outras regras
@starting-style pode ser escrito dentro do seletor (CSS nesting) ou fora como bloco separado. As duas formas são equivalentes:
/* Forma 1: separado */
.card { opacity: 1; transition: opacity .3s; }
@starting-style { .card { opacity: 0; } }
/* Forma 2: aninhado (CSS nesting) */
.card {
opacity: 1;
transition: opacity .3s;
@starting-style {
opacity: 0;
}
}
A forma aninhada é mais legível — o estado inicial fica junto com o seletor ao qual pertence.
Suporte
Chrome 117+, Edge 117+, Firefox 129+, Safari 17.5+. Baseline amplamente disponível. Sem necessidade de fallback para projetos que descartam browsers legados.
@starting-style fecha a lacuna mais frustrante das animações CSS. Junto com a Popover API e o Anchor Positioning, forma uma trinca que elimina boa parte do JavaScript escrito hoje apenas para controlar aparência. Vale aprender os três juntos — eles se complementam.