Scroll-driven animations com CSS puro: sem biblioteca, sem JavaScript
1. Introdução às Scroll-driven Animations
Scroll-driven animations representam uma mudança de paradigma no desenvolvimento web. Pela primeira vez, podemos criar animações diretamente vinculadas ao scroll do usuário usando apenas CSS — sem uma única linha de JavaScript, sem Intersection Observer, sem bibliotecas como ScrollMagic ou GSAP.
O que torna essas animações revolucionárias é a capacidade de sincronizar automaticamente a progressão de uma animação com a posição do scroll. Antes, isso exigia cálculos constantes de posição, listeners de eventos e manipulação manual do DOM. Agora, o navegador faz todo o trabalho pesado de forma nativa e otimizada.
O suporte atual é promissor: Chrome 115+, Edge 115+ e Opera já implementam completamente a especificação. Firefox está em desenvolvimento ativo, e Safari sinalizou interesse. Para produção, é essencial considerar fallbacks, mas o futuro é claro.
2. Conceitos Fundamentais: Scroll Timeline vs View Timeline
Existem dois tipos principais de timelines em scroll-driven animations:
Scroll Timeline (scroll-timeline): A animação progride conforme o scroll de um contêiner específico. O início da animação corresponde ao scroll no topo, e o fim corresponde ao scroll no final do contêiner.
View Timeline (view-timeline): A animação progride conforme um elemento específico se move através da viewport. A animação começa quando o elemento aparece na tela e termina quando ele sai completamente.
As funções scroll() e view() são atalhos práticos para usar essas timelines diretamente na propriedade animation-timeline:
/* Scroll Timeline: vinculada ao scroll do documento */
.elemento {
animation-timeline: scroll();
}
/* View Timeline: vinculada à visibilidade do próprio elemento */
.elemento {
animation-timeline: view();
}
3. Criando Animações com animation-timeline
A sintaxe básica é surpreendentemente simples. Vamos criar uma barra de progresso de leitura sem JavaScript:
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
height: 200vh;
font-family: system-ui, sans-serif;
}
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 4px;
background: linear-gradient(90deg, #667eea, #764ba2);
width: 0%;
animation: fillProgress linear;
animation-timeline: scroll();
}
@keyframes fillProgress {
from { width: 0%; }
to { width: 100%; }
}
.content {
max-width: 800px;
margin: 60px auto;
padding: 20px;
line-height: 1.8;
}
</style>
</head>
<body>
<div class="progress-bar"></div>
<div class="content">
<h1>Artigo extenso</h1>
<p>Conteúdo longo que ocupa bastante espaço vertical...</p>
<!-- Repetir parágrafos para gerar scroll -->
<p style="margin-top: 200vh">Fim do artigo</p>
</div>
</body>
</html>
A mágica está em animation-timeline: scroll() — sem eventos, sem cálculos, sem JavaScript.
4. Trabalhando com Ranges e Keyframes Específicos
Os ranges permitem controlar exatamente quando a animação deve começar e terminar em relação à viewport. Os principais valores são:
entry: do momento em que o elemento começa a entrar até estar completamente visívelexit: do momento em que o elemento começa a sair até estar completamente foracover: do início da entrada até o fim da saída (ciclo completo)contain: quando o elemento está completamente dentro da viewport
Exemplo de fade-in e fade-out conforme o elemento cruza a tela:
.fade-card {
animation: fadeInOut linear;
animation-timeline: view();
animation-range: entry 0% exit 100%;
}
@keyframes fadeInOut {
entry 0% {
opacity: 0;
transform: translateY(50px);
}
entry 100% {
opacity: 1;
transform: translateY(0);
}
exit 0% {
opacity: 1;
transform: translateY(0);
}
exit 100% {
opacity: 0;
transform: translateY(-50px);
}
}
5. Animações com Múltiplos Elementos e Efeitos de Parallax
Para criar um efeito parallax nativo, combinamos animation-timeline com transform em diferentes velocidades:
.parallax-layer {
position: fixed;
width: 100%;
height: 100vh;
animation: parallaxScroll linear;
animation-timeline: scroll();
}
.layer-back {
animation-range: 0% 200%;
z-index: 1;
}
.layer-front {
animation-range: 0% 100%;
z-index: 2;
}
@keyframes parallaxScroll {
from { transform: translateY(0); }
to { transform: translateY(-50%); }
}
Para escalar em diferentes telas, use unidades relativas e evite valores fixos em pixels nos keyframes.
6. Controlando Direção, Velocidade e Repetição
Scroll-driven animations aceitam as mesmas propriedades de animação tradicionais:
.elemento {
animation: slideIn linear;
animation-timeline: scroll();
animation-direction: alternate;
animation-fill-mode: both;
animation-iteration-count: 1;
}
A velocidade relativa ao scroll é controlada indiretamente pelo animation-range. Um range maior significa que a animação se estende por mais scroll, parecendo mais lenta.
Para animações mistas (scroll-driven + time-driven), use timelines diferentes:
.elemento {
animation:
pulse 2s ease-in-out infinite, /* time-driven */
slideIn linear scroll(); /* scroll-driven */
}
7. Limitações, Fallbacks e Boas Práticas
Nem todos os navegadores suportam scroll-driven animations. Use @supports para fallbacks:
@supports (animation-timeline: scroll()) {
.animated-element {
animation: fadeIn linear;
animation-timeline: view();
}
}
@supports not (animation-timeline: scroll()) {
.animated-element {
opacity: 1;
transform: none;
}
}
Respeite prefers-reduced-motion:
@media (prefers-reduced-motion: reduce) {
.animated-element {
animation: none !important;
}
}
Performance: scroll-driven animations são executadas no compositor do navegador, fora da thread principal. São inerentemente mais eficientes que alternativas em JavaScript, especialmente em dispositivos móveis.
Use scroll-driven animations para: barras de progresso, fade-in de elementos, parallax sutil, indicadores de seção. Evite para: animações complexas com múltiplos estágios ou que exigem lógica condicional.
8. Exemplo Completo: Landing Page com Scroll-driven Animations
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: system-ui, sans-serif;
background: #0f0f23;
color: #e0e0e0;
overflow-x: hidden;
}
/* Barra de progresso */
.progress {
position: fixed;
top: 0;
left: 0;
height: 3px;
background: linear-gradient(90deg, #ff6b6b, #ffd93d);
z-index: 100;
animation: fillProgress linear;
animation-timeline: scroll();
}
@keyframes fillProgress {
from { width: 0%; }
to { width: 100%; }
}
/* Hero com parallax */
.hero {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.hero-bg {
position: absolute;
width: 100%;
height: 120%;
background: radial-gradient(circle at 50% 50%, #1a1a4e, #0f0f23);
animation: parallaxBg linear;
animation-timeline: scroll();
animation-range: 0% 100%;
}
@keyframes parallaxBg {
from { transform: translateY(0); }
to { transform: translateY(-20%); }
}
.hero-content {
position: relative;
z-index: 1;
text-align: center;
animation: heroFade linear;
animation-timeline: scroll();
animation-range: 0% 50%;
}
@keyframes heroFade {
from { opacity: 1; transform: scale(1); }
to { opacity: 0; transform: scale(0.8); }
}
/* Seções com view timeline */
section {
min-height: 60vh;
padding: 80px 20px;
max-width: 800px;
margin: 0 auto;
}
.card {
background: rgba(255,255,255,0.05);
border-radius: 16px;
padding: 40px;
margin: 40px 0;
border: 1px solid rgba(255,255,255,0.1);
animation: cardReveal linear;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes cardReveal {
from {
opacity: 0;
transform: translateY(60px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* Imagem com fade-in */
.image-wrapper {
margin: 40px 0;
overflow: hidden;
border-radius: 12px;
}
.image-wrapper img {
width: 100%;
height: auto;
display: block;
animation: imageZoom linear;
animation-timeline: view();
animation-range: entry 0% exit 100%;
}
@keyframes imageZoom {
entry 0% { transform: scale(1.1); opacity: 0; }
entry 100% { transform: scale(1); opacity: 1; }
exit 0% { transform: scale(1); opacity: 1; }
exit 100% { transform: scale(0.9); opacity: 0; }
}
/* Footer */
footer {
text-align: center;
padding: 60px 20px;
opacity: 0.6;
animation: footerFade linear;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes footerFade {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 0.6; transform: translateY(0); }
}
/* Fallback para navegadores sem suporte */
@supports not (animation-timeline: scroll()) {
.hero-content { opacity: 1; }
.card { opacity: 1; transform: none; }
.image-wrapper img { opacity: 1; transform: none; }
footer { opacity: 0.6; }
}
/* Acessibilidade */
@media (prefers-reduced-motion: reduce) {
.progress, .hero-bg, .hero-content,
.card, .image-wrapper img, footer {
animation: none !important;
}
}
h1 { font-size: 3.5rem; margin-bottom: 20px; }
h2 { font-size: 2rem; margin-bottom: 20px; color: #ffd93d; }
p { line-height: 1.8; font-size: 1.1rem; }
</style>
</head>
<body>
<div class="progress"></div>
<section class="hero">
<div class="hero-bg"></div>
<div class="hero-content">
<h1>Scroll-driven Animations</h1>
<p>CSS puro, zero JavaScript</p>
</div>
</section>
<section>
<h2>Como funciona</h2>
<div class="card">
<p>Scroll-driven animations vinculam a progressão da animação diretamente ao scroll do usuário. Cada card aparece suavemente ao entrar na viewport.</p>
</div>
<div class="card">
<p>Usando <code>animation-timeline: view()</code>, o navegador calcula automaticamente quando cada elemento deve começar e terminar sua animação.</p>
</div>
<div class="card">
<p>Não há Intersection Observer, não há event listeners, não há JavaScript. Tudo é nativo e otimizado pelo navegador.</p>
</div>
</section>
<section>
<h2>Efeitos visuais</h2>
<div class="image-wrapper">
<img src="https://picsum.photos/800/400?random=1" alt="Exemplo visual">
</div>
<div class="card">
<p>Imagens podem ter zoom suave, fade e parallax — tudo controlado por ranges como <code>entry</code> e <code>exit</code>.</p>
</div>
</section>
<footer>
<p>© 2025 — Exemplo didático de scroll-driven animations com CSS puro</p>
</footer>
</body>
</html>
Este exemplo completo demonstra barra de progresso, parallax no hero, fade-in de cards, zoom em imagens e footer animado — tudo sem JavaScript. A página é responsiva, acessível e inclui fallbacks para navegadores sem suporte.
Referências
- Scroll-driven Animations - CSS-Tricks — Guia completo com exemplos práticos e explicações detalhadas sobre scroll e view timelines
- MDN Web Docs: animation-timeline — Documentação oficial da propriedade
animation-timelinecom sintaxe e exemplos - Chrome Developers: Scroll-driven animations — Artigo técnico da equipe Chrome explicando a implementação e casos de uso
- W3C Specification: Scroll-driven Animations — Especificação oficial do W3C para scroll-driven animations
- web.dev: Animate elements on scroll with Scroll-driven animations — Tutorial prático com demonstrações interativas e dicas de performance
- Codepen: Collection of Scroll-driven Animations — Coleção curada de exemplos funcionais para inspiração e estudo