Criando Animações de Scroll Nativas com animation-timeline e scroll() Sem JavaScript
O problema que JavaScript resolveu por tempo demais
Scroll listeners em JavaScript são caros. Cada evento scroll dispara dezenas de vezes por segundo, executa no main thread e, se o handler toca no DOM, força recalculations de layout. A solução clássica envolve requestAnimationFrame, IntersectionObserver ou bibliotecas como GSAP ScrollTrigger, tudo para conectar uma propriedade CSS ao progresso de scroll.
A especificação CSS Scroll-driven Animations (parte do CSS Animations Level 2) resolve isso na camada do navegador. O compositor calcula o progresso do scroll e aplica a animação sem tocar no main thread. O resultado: animações que rodam a 60fps mesmo em páginas pesadas, com zero bytes de JavaScript.
Este post cobre animation-timeline, scroll() e view(), com código funcional que você cola no projeto e testa agora.
Como animation-timeline funciona por baixo
Uma animação CSS tradicional usa tempo como eixo de progresso. Você define animation-duration: 1s e o navegador interpola os keyframes ao longo de 1 segundo. Com animation-timeline, o eixo de progresso deixa de ser tempo e passa a ser a posição de scroll de um container.
/* O navegador mapeia 0% do scroll → 0% dos keyframes
e 100% do scroll → 100% dos keyframes.
animation-duration ainda é necessário sintaticamente,
mas o valor é ignorado: quem controla é o scroll. */
.progress-bar {
animation: grow-width linear;
animation-timeline: scroll();
}
@keyframes grow-width {
from { width: 0%; }
to { width: 100%; }
}
Dois tipos de timeline existem:
| Tipo | Função CSS | Eixo de progresso | Caso de uso típico |
|---|---|---|---|
| Scroll Progress | scroll() | Posição do scroll de um container (0% = topo, 100% = fim) | Barra de progresso de leitura, parallax global |
| View Progress | view() | Visibilidade de um elemento no viewport (0% = entra, 100% = sai) | Fade-in ao scroll, reveal de cards |
A diferença entre scroll() e view() define o tipo de animação que você consegue construir.
Barra de progresso de leitura com scroll()
O caso mais direto: uma barra fixa no topo que cresce conforme o usuário rola a página.
<!-- O elemento fica fixo no topo da viewport -->
<div class="reading-progress" aria-hidden="true"></div>
<article class="article-content">
<h1>Título do artigo</h1>
<p>Conteúdo longo aqui...</p>
</article>
.reading-progress {
position: fixed;
top: 0;
left: 0;
height: 4px;
background: #3b82f6;
width: 0%;
z-index: 50;
/* animation-duration é obrigatório pela spec,
mas o valor não importa: o scroll controla tudo */
animation: progress-grow linear;
animation-timeline: scroll(root block);
}
@keyframes progress-grow {
from { width: 0%; }
to { width: 100%; }
}
A função scroll() aceita dois argumentos: o scroller (root, nearest, self) e o eixo (block, inline). scroll(root block) significa "use o scroll vertical do documento inteiro". Se você omitir os argumentos, o padrão é scroll(nearest block), que busca o ancestral scrollable mais próximo.
Reveal animation com view()
Para animar elementos conforme entram no viewport, view() substitui o IntersectionObserver que você usaria em JavaScript.
.card {
opacity: 0;
transform: translateY(40px);
animation: card-reveal linear both;
animation-timeline: view();
/* animation-range controla QUANDO a animação começa e termina
dentro do ciclo de visibilidade do elemento.
entry 0% = o elemento começa a entrar no viewport
entry 100% = o elemento está completamente visível */
animation-range: entry 0% entry 100%;
}
@keyframes card-reveal {
from {
opacity: 0;
transform: translateY(40px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
<div class="card-grid">
<div class="card">
<h3>Feature 1</h3>
<p>Descrição da feature com texto real.</p>
</div>
<div class="card">
<h3>Feature 2</h3>
<p>Outra descriç
---
Leia o artigo completo em [https://www.vivodecodigo.com.br/react/animacoes-scroll-css-animation-timeline-scroll-sem-javascript](https://www.vivodecodigo.com.br/react/animacoes-scroll-css-animation-timeline-scroll-sem-javascript)