1

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

Carregando publicação patrocinada...