Animações de Scroll com CSS Puro: animation-timeline e scroll() na Prática
Animações de Scroll com CSS Puro: animation-timeline e scroll() na Prática
A barra de progresso de leitura que você vê em blogs como o Medium. O fade-in de seções conforme o usuário rola a página. O efeito parallax sutil em hero sections. Até pouco tempo, cada um desses efeitos exigia JavaScript: IntersectionObserver, scroll event listeners, ou bibliotecas como GSAP ScrollTrigger e Framer Motion.
Com animation-timeline e scroll(), o CSS resolve esses três cenários sem uma linha de JavaScript. Zero overhead de runtime, zero bundle size adicional, zero listeners no main thread.
Como funciona: a timeline deixa de ser tempo e passa a ser scroll
Uma animação CSS tradicional usa o 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: em vez de milissegundos, o progresso da animação acompanha a posição de scroll de um container.
/* O progresso da animação vai de 0% a 100%
conforme o scroll do elemento root (viewport) */
.progress-bar {
animation: grow-width linear;
animation-timeline: scroll();
}
@keyframes grow-width {
from {
width: 0%;
}
to {
width: 100%;
}
}
A função scroll() aceita dois argumentos opcionais: o scroller (qual elemento observar) e o eixo (block ou inline). Quando chamada sem argumentos, usa o nearest scroll ancestor no eixo block (vertical).
Progress bar de leitura: o exemplo canônico
Esse é o caso de uso que melhor demonstra o poder da feature. Em JavaScript, você precisaria de um scroll event listener, calcular scrollTop / (scrollHeight - clientHeight), e atualizar o width via style.width ou uma CSS custom property. Com animation-timeline, são 15 linhas de CSS:
.reading-progress {
position: fixed;
top: 0;
left: 0;
height: 4px;
width: 0%;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
z-index: 9999;
/* animation-duration é ignorado quando animation-timeline
está ativo, mas o browser exige um valor válido */
animation: fill-bar linear;
animation-timeline: scroll(root block);
}
@keyframes fill-bar {
to {
width: 100%;
}
}
<!-- O elemento precisa existir no DOM, mas não precisa de JS -->
<div class="reading-progress" aria-hidden="true"></div>
<main>
<article>
<!-- conteúdo longo aqui -->
</article>
</main>
O argumento root em scroll(root block) garante que o scroller é o viewport, não um container intermediário com overflow. Isso é relevante quando a progress bar está dentro de um layout com scroll aninhado.
Fade-in de seções com view() e animation-range
A função scroll() observa o progresso total de scroll de um container. Para animar elementos conforme eles entram no viewport (o padrão "reveal on scroll"), a função certa é view(). Ela cria uma timeline baseada na visibilidade do próprio elemento dentro do scroll container:
.reveal-section {
opacity: 0;
transform: translateY(30px);
animation: fade-in-up linear both;
animation-timeline: view();
/* A animação acontece entre o momento em que o elemento
começa a entrar no viewport e quando 30% dele está visível.
Sem animation-range, a animação se espalha por toda
a passagem do elemento pelo viewport, ficando lenta demais. */
animation-range: entry 0% entry 30%;
}
@keyframes fade-in-up {
to {
opacity: 1;
transform: translateY(0);
}
}
<section class="reveal-section">
<h2>Arquitetura do projeto</h2>
<p>Conteúdo que aparece com fade-in ao rolar.</p>
</section>
<section class="reveal-section">
<h2>Stack de deploy</h2>
<p>Mais conteúdo com o mesmo efeito.</p>
</section>
O animation-range é o que diferencia uma animação suave de uma animação arrastada. entry 0% é quando a borda inferior do elemento toca a borda inferior do viewport. entry 30% é quando 30% do elemento já está visível. Sem esse range, a animação se distribui por toda a travessia
Leia o artigo completo em https://www.vivodecodigo.com.br/react/animacoes-scroll-nativas-css-animation-timeline-scroll-1787745710133