1

Animações de Scroll Nativas com animation-timeline e scroll()

A barra de progresso de leitura que você vê em blogs como Medium, Dev.to e até aqui: até 2023, ela exigia JavaScript. Um scroll event listener, cálculo de scrollTop / scrollHeight, atualização de width via style ou transform. Dezenas de linhas para algo que deveria ser declarativo.

Com animation-timeline: scroll(), é declarativo. O CSS ganha uma primitiva que vincula qualquer animação ao progresso de scroll de um container, sem uma linha de JavaScript. Sem requestAnimationFrame, sem Intersection Observer, sem bibliotecas de 40kB.

Este post mostra como usar essa API na prática, com código que você cola e roda. Onde ela funciona, onde ainda não funciona, e onde JavaScript continua sendo a escolha correta.

O modelo mental: scroll como timeline

Animações CSS tradicionais usam tempo como eixo. Você define animation-duration: 1s e o browser interpola os keyframes ao longo de 1 segundo. Com animation-timeline, o eixo muda de tempo para progresso de scroll. O início do scroll (topo) mapeia para 0% dos keyframes. O fim do scroll mapeia para 100%.

Duas funções controlam isso:

  • scroll(): vincula a animação ao progresso de scroll de um container (o documento inteiro ou um elemento com overflow).
  • view(): vincula a animação à visibilidade de um elemento dentro do viewport (equivalente ao que Intersection Observer faz).

A diferença entre as duas é o referencial. scroll() pergunta "quanto o container scrollou?". view() pergunta "quanto deste elemento está visível?".

Barra de progresso de leitura: zero JavaScript

O caso de uso mais direto. Uma barra fixa no topo que cresce conforme o usuário desce a página.

/* A barra cresce de 0% a 100% conforme o scroll do documento */
.reading-progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 4px;
  background: #3b82f6;
  width: 100%;
  transform-origin: left;
  /* scaleX é mais performático que width porque roda no compositor */
  transform: scaleX(0);
  animation: grow-bar linear;
  /* scroll() sem argumentos usa o nearest scroll ancestor, que aqui é o root */
  animation-timeline: scroll();
  z-index: 9999;
}

@keyframes grow-bar {
  to {
    transform: scaleX(1);
  }
}
<!-- Coloque como primeiro filho do body -->
<div class="reading-progress" aria-hidden="true"></div>

Dois detalhes que importam nesse código. Primeiro: animation-duration não aparece. Quando animation-timeline está definido, o browser ignora animation-duration e usa o progresso do scroll como eixo. Segundo: scaleX em vez de width. Animar width dispara layout recalculation a cada frame de scroll. scaleX roda inteiramente no compositor thread, sem layout, sem paint.

Reveal on scroll com view()

Elementos que aparecem conforme entram no viewport: o padrão "fade in on scroll" que normalmente exige Intersection Observer ou ScrollTrigger do GSAP.

.reveal-card {
  opacity: 0;
  transform: translateY(40px);
  animation: reveal-up linear both;
  animation-timeline: view();
  /* entry 0% = elemento começa a entrar; entry 100% = elemento totalmente visível */
  animation-range: entry 0% entry 100%;
}

@keyframes reveal-up {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
<section class="content-grid">
  <article class="reveal-card">
    <h3>Primeiro card</h3>
    <p>Conteúdo que aparece ao scrollar.</p>
  </article>
  <article class="reveal-card">
    <h3>Segundo card</h3>
    <p>Cada card anima independentemente.</p>
  </article>
  <article class="reveal-card">
    <h3>Terceiro card</h3>
    <p>Sem JavaScript, sem observer, sem cleanup.</p>
  </article>
</section>

O animation-range é a parte mais poderosa e menos intuitiva. Ele define em qual trecho da timeline a animação acontece. entry 0% significa "o momento em que a borda inferior do elemento toca a borda inferior do viewport". entry 100% significa "o elemento está completamente dentro do viewport". Sem animation-range, a animação se espalha por todo o


Leia o artigo completo em https://www.vivodecodigo.com.br/react/animacoes-scroll-nativas-css-animation-timeline-scroll

Carregando publicação patrocinada...