CSS

@starting-style: animando elementos que entram no DOM com CSS puro

A regra CSS que resolve o problema mais chato de animação: elementos que aparecem do nada. Chega de setTimeout para fingir uma transição de entrada.

· 6 min de leitura

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

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.