CSS scroll-driven: o que já dá para usar sem JS
Dois dos três motores enviaram scroll-driven animations. O terceiro está atrás de flag. O que isso muda no que você pode usar em produção hoje.
- css
- motion

Sumário
- Qual é o suporte real hoje
- scroll() ou view(): qual das duas você quer
- A armadilha que quebra sem avisar
- O que degrada sozinho e o que quebra feio
- O @supports que resolve
- Quando o IntersectionObserver continua sendo a resposta
- O que "compositor thread" muda na prática
- Receita completa: reveal que funciona nos três motores
- Quando não anima: a ordem de checagem
- Perguntas frequentes
- Fontes
Dá para usar em produção hoje, em Chrome, Edge e Safari. Não dá para aposentar o IntersectionObserver, porque o Firefox ainda entrega a feature atrás de flag.
Essa distinção não é preciosismo. Ela decide se o seu efeito vira degradação silenciosa ou seção invisível em um motor inteiro — e a maioria dos textos sobre o assunto hoje recomenda exatamente o caminho que produz o segundo caso.
Este guia é o mapa do que já está seguro, do que precisa de fallback explícito e da armadilha de sintaxe que quebra a animação sem dar erro nenhum no console.
Qual é o suporte real hoje
Dois dos três motores enviaram. O terceiro implementou e não ligou.
| Motor | Estado | Desde |
|---|---|---|
| Chrome / Edge (Blink) | Enviado, sem flag | 115 — julho de 2023 |
| Safari (WebKit) | Enviado | 26 — setembro de 2025 |
| Firefox (Gecko) | Atrás de flag | Padrão só no Nightly |
O Safari não parou na estreia: a 26.4 trouxe as scroll-driven animations threaded, e a 26.5, de 11 de maio de 2026, trouxe quatro correções de confiabilidade — entre elas o suporte ao nome de range scroll e um bug em que a animação não pausava quando animation-play-state virava paused dinamicamente (WebKit, acesso em 19/08/2026).
No Firefox, o bug meta 1676780 — Implement scroll-driven animations generated by CSS segue com status NEW, aberto há cinco anos e ainda com 15 dependências ativas. A ativação no Nightly saiu pelo 1817303, RESOLVED FIXED, com alvo no Firefox 136. Ou seja: o Nightly liga a preferência desde a 136, mas o meta de implementação não fechou. Quem quiser testar hoje liga layout.css.scroll-driven-animations.enabled no about:config.
É por isso que o MDN carimba a propriedade como "Limited availability — this feature is not Baseline because it does not work in some of the most widely-used browsers" (MDN, acesso em 19/08/2026).
Traduzindo para decisão de projeto: use como camada, não como fundação.
scroll() ou view(): qual das duas você quer
São duas linhas do tempo diferentes, e trocar uma pela outra é o erro mais comum.
scroll() mede o progresso do contêiner de rolagem. A animação anda conforme a barra de rolagem anda, do topo ao fim. Serve para barra de progresso de leitura, indicador de posição, qualquer coisa atrelada ao documento inteiro.
.barra-progresso {
animation: preencher linear;
animation-timeline: scroll(y root);
}
@keyframes preencher {
from { scale: 0 1; }
to { scale: 1 1; }
}O scroll() aceita eixo (block, inline, x, y) e scroller (root, nearest, self).
view() mede o progresso de um elemento atravessando a viewport. A animação anda conforme aquele elemento entra e sai de vista. Serve para reveal, parallax por item, qualquer coisa atrelada a um componente.
.card {
animation: revelar linear;
animation-timeline: view();
animation-range: entry 0% cover 40%;
}
@keyframes revelar {
from { opacity: 0; translate: 0 2rem; }
to { opacity: 1; translate: 0 0; }
}O animation-range é o que dá controle fino. Os nomes de intervalo disponíveis são entry, entry-crossing, contain, cover e exit. No exemplo acima, a animação começa quando o card encosta na viewport e termina quando ele já percorreu 40% da travessia — não no fim dela.
Quando precisar que um elemento seja animado pela rolagem de outro, nomeie a timeline:
.secao { view-timeline: --secao block; }
.legenda { animation: deslizar linear; animation-timeline: --secao; }
.container { timeline-scope: --secao; }O timeline-scope existe porque uma timeline nomeada só é visível para descendentes de quem a declarou. Sem ele, elementos irmãos não se enxergam.
Os cinco nomes de intervalo
O animation-range é a parte que mais rende e a menos explicada. Os cinco nomes descrevem momentos diferentes da travessia do elemento pela viewport:
| Nome | Cobre |
|---|---|
cover | Da primeira borda entrando até a última saindo. É o intervalo completo |
entry | Só a entrada: da borda tocando a viewport até estar inteiramente dentro |
exit | Só a saída: de começar a sair até desaparecer |
contain | O período em que o elemento está inteiramente visível |
entry-crossing | O cruzamento da borda durante a entrada |
Na prática, entry resolve quase todo reveal, e cover serve para parallax que precisa durar a travessia inteira. Misturar os dois em um só range (entry 0% cover 40%) dá começo na entrada e fim no meio da travessia, que costuma ser o timing mais natural para conteúdo de leitura.
A armadilha que quebra sem avisar
animation-timeline é reset-only dentro do atalho animation. Ou seja: escrever o atalho depois apaga a timeline e devolve ela para auto, silenciosamente.
/* QUEBRADO — a timeline vira auto e a animação roda no tempo */
.card {
animation-timeline: view();
animation: revelar linear;
}
/* CORRETO — declare a timeline depois do atalho */
.card {
animation: revelar linear;
animation-timeline: view();
}Não há erro no console. A animação simplesmente roda como animação normal de tempo, dispara no load e você passa vinte minutos procurando o que está errado no @keyframes.
Duas regras menores da mesma família, também documentadas pelo MDN: quando duas timelines compartilham o mesmo <dashed-ident> e a mesma especificidade, vale a última declarada na cascata; e quando há menos valores de animation-timeline que de animation-name, os valores de timeline se repetem.
O que degrada sozinho e o que quebra feio
Aqui está a tabela que decide se você precisa de fallback ou não. A pergunta é sempre a mesma: como fica o elemento se a timeline nunca avançar?
| Efeito | Sem suporte | Precisa de fallback? |
|---|---|---|
| Barra de progresso de leitura | Fica em 0%, invisível ou vazia | Não — é enfeite |
| Parallax de fundo | Fundo estático | Não |
| Sombra ou cor que muda no scroll | Fica no estado inicial | Não |
| Rotação ou escala decorativa | Fica no estado inicial | Não |
Reveal com opacity: 0 | Conteúdo invisível para sempre | Sim, obrigatório |
Entrada com translate grande | Conteúdo fora da tela | Sim, obrigatório |
Item com visibility: hidden | Conteúdo inacessível | Sim, obrigatório |
O padrão é claro: efeito que parte do estado final degrada sozinho; efeito que parte de um estado escondido deixa o conteúdo escondido.
E é exatamente o segundo grupo que os tutoriais usam como exemplo de abertura, porque reveal é o efeito mais vistoso. Copiar aquele trecho sem fallback é publicar um site cujas seções não aparecem no Firefox.
O @supports que resolve
A regra é escrever o CSS assumindo que não há suporte, e só então ligar a animação.
/* Estado padrão: visível. Funciona em qualquer motor. */
.card { opacity: 1; translate: none; }
@supports (animation-timeline: view()) {
.card {
animation: revelar linear;
animation-timeline: view();
animation-range: entry 0% cover 40%;
}
@keyframes revelar {
from { opacity: 0; translate: 0 2rem; }
to { opacity: 1; translate: 0 0; }
}
}O ponto que faz isso funcionar: o opacity: 0 mora dentro do @keyframes, e o @keyframes mora dentro do @supports. Quem não tem suporte nunca encontra a regra que esconde o conteúdo.
O erro comum é o inverso — deixar .card { opacity: 0 } no CSS base e tentar reverter no @supports not. Funciona, mas depende de você lembrar de reverter tudo, e uma propriedade esquecida vira conteúdo invisível.
Vale respeitar também quem pediu menos movimento:
@media (prefers-reduced-motion: reduce) {
.card { animation: none; }
}Quando o IntersectionObserver continua sendo a resposta
Não é sobre suporte. Há coisas que as scroll-driven animations não fazem, por definição.
Quando você precisa executar código, não animar. Disparar analytics, carregar imagem, iniciar vídeo, marcar item como lido. A timeline do CSS anima propriedades; ela não chama função.
Quando o efeito é de uma via só. Scroll-driven é reversível por natureza: subiu, desfaz. Se você quer que um item apareça e fique, o IntersectionObserver com unobserve é mais simples que qualquer combinação de animation-fill-mode e range.
Quando o gatilho não é geométrico. Um contador que começa quando o elemento aparece, um lazy-load com margem customizada, um sticky header que muda de estado a partir de um limiar específico.
Quando o suporte precisa ser universal hoje. Site institucional, checkout, formulário. Ali o Firefox não é detalhe.
A combinação que uso: CSS para o que é decorativo e reversível, IntersectionObserver para o que dispara comportamento.
A escolha entre biblioteca e CSS puro tem uma árvore de decisão em GSAP, Motion ou CSS puro.
O que "compositor thread" muda na prática
Muda a fluidez quando a main thread está ocupada.
Uma animação de scroll feita em JavaScript roda na main thread. Ela compete com hidratação de React, parsing de JSON, handler de evento e qualquer script de terceiro. Quando a main thread trava por 200 ms, a animação trava junto — é a origem daquele scroll que "engasga" em página pesada.
A Safari 26.4 passou as scroll-driven animations para o compositor thread, dando a elas o mesmo tratamento que as transitions e keyframes já tinham. O Chrome faz isso desde o envio.
O ganho está em a animação continuar suave enquanto o resto da página está lento — que é justamente o momento em que a versão em JavaScript falha.
Uma ressalva honesta: nem toda propriedade animada vai para o compositor. Animar opacity e transform fica lá; animar width, height ou top força layout na main thread e devolve o problema. A escolha da propriedade continua importando tanto quanto antes.
Se a preocupação for a métrica que o Google mede, vale saber que rolagem não entra no INP — o custo aparece em outro momento.
Receita completa: reveal que funciona nos três motores
Junta tudo. Copiável.
/* 1. Base: conteúdo visível, sem depender de nada */
.reveal {
opacity: 1;
translate: none;
}
/* 2. Camada scroll-driven, só onde existe suporte */
@supports (animation-timeline: view()) {
.reveal {
animation: reveal-up linear both;
animation-timeline: view(); /* depois do atalho, sempre */
animation-range: entry 10% cover 35%;
}
@keyframes reveal-up {
from { opacity: 0; translate: 0 1.5rem; }
to { opacity: 1; translate: 0 0; }
}
}
/* 3. Respeita quem pediu menos movimento */
@media (prefers-reduced-motion: reduce) {
.reveal { animation: none; opacity: 1; translate: none; }
}No Chrome, Edge e Safari o conteúdo entra com o scroll. No Firefox ele aparece normalmente, sem animação e sem buraco. Em qualquer motor com prefers-reduced-motion, ele aparece direto.
Sem JavaScript, sem biblioteca, sem observer.
O catálogo completo de técnicas de motion para web cobre as outras abordagens, incluindo as que ainda exigem JavaScript.
Quando não anima: a ordem de checagem
Scroll-driven falha em silêncio. Não há exceção, não há aviso no console — a animação simplesmente não acontece, ou acontece na hora errada. Checar nesta ordem economiza tempo.
1. A ordem no atalho. É a causa mais comum, de longe. animation-timeline tem que vir depois de animation. Se estiver antes, a timeline foi resetada para auto.
2. O motor tem suporte? Coloque um detector visível enquanto desenvolve:
@supports not (scroll-timeline: --teste) {
body::before {
content: "Este browser não suporta scroll-driven animations.";
position: fixed; inset-block-start: 0; inset-inline-start: 0;
padding: .5rem 1rem; background: #C4552F; color: #fff; z-index: 9999;
}
}Se a tarja aparecer no Firefox e não no Chrome, o CSS está certo e o problema é suporte, não sintaxe.
3. O scroller é o que você acha que é? scroll() sem argumento usa o scroller mais próximo. Se um ancestral tem overflow: hidden ou overflow: auto, ele vira o scroller — e como ele não rola, a timeline nunca avança. Teste com scroll(root) para isolar.
4. A timeline nomeada está no escopo? Timeline nomeada só enxerga descendentes. Para irmãos, timeline-scope no ancestral comum.
5. O elemento tem altura? view() precisa que o elemento tenha dimensão para atravessar a viewport. Elemento com height: 0 ou colapsado nunca completa o intervalo.
6. prefers-reduced-motion está ativo? Se você escreveu a regra de acessibilidade, ela está funcionando. Cheque o sistema antes de suspeitar do CSS.
7. Você atualizou o framework? O Next.js 16 parou de sobrescrever scroll-behavior: smooth durante navegação de rota. Não quebra scroll-driven, mas muda o comportamento da navegação num site que tem smooth scroll global.
Animação scroll-driven roda no compositor e não passa por JavaScript, então só a consulta de mídia a controla. Como fazer isso sem entregar uma página estática está em prefers-reduced-motion.
Para efeito tipográfico, vale conferir antes se o caso cabe em clip-path ou background-clip, tratados em animação de texto.
Perguntas frequentes
Posso usar em produção hoje?
Sim, como camada de progressive enhancement. Chrome, Edge e Safari cobrem a maior parte do tráfego brasileiro, e o Firefox degrada bem desde que você use o padrão @supports acima. O que não dá é tratar como substituto do IntersectionObserver em efeito que esconde conteúdo.
Preciso de polyfill?
Não recomendo. Um polyfill de scroll-driven animations devolve o trabalho para a main thread, que é justamente o problema que a feature nativa resolve. Você paga o custo de performance para ganhar suporte em um motor que já degrada de forma aceitável.
Isso substitui o GSAP e o ScrollTrigger?
Para reveal, parallax simples e barra de progresso, sim. Para timeline com várias etapas, pinning de seção, scrub coordenado entre elementos e sequência com controle de easing por trecho, não. Os padrões de motion que dissequei em sites de agência quase sempre caem nesse segundo grupo. As duas ferramentas convivem bem: CSS no que é simples e reversível, GSAP no que é orquestração.
Por que minha animação dispara sozinha no carregamento?
Quase sempre é a ordem no atalho. Se animation: nome linear aparece depois de animation-timeline, a timeline é resetada para auto e a animação roda no tempo. Mova animation-timeline para depois do atalho.
Fontes
- MDN —
animation-timeline. Acesso em 19/08/2026. - MDN — CSS scroll-driven animations. Acesso em 19/08/2026.
- WebKit — WebKit Features for Safari 26.5. Acesso em 19/08/2026.
- WebKit — A guide to Scroll-driven Animations with just CSS. Acesso em 19/08/2026.
- Bugzilla — 1676780, meta de scroll-driven animations no Firefox. Acesso em 19/08/2026.
- Bugzilla — 1817303, ativação no Nightly. Acesso em 19/08/2026.
Verificado em 19 de agosto de 2026.
Gatilho de reavaliação: este post precisa de revisão quando (a) o Firefox ligar a flag por padrão em release estável, o que move a feature para Baseline e muda a recomendação central, (b) o MDN alterar o carimbo de Limited availability, ou (c) surgir mudança de sintaxe em animation-range ou timeline-scope.
Leia também
Motion •
GSAP, Motion ou CSS: a árvore de decisão
A pergunta mudou: metade do que exigia biblioteca hoje é CSS nativo. Os eixos que sobraram são licença, modelo de execução e o que já é nativo.
- gsap
- motion
Motion •
Motion Design para Web: Guia Completo
Scroll, texto, imagens e vídeo: o catálogo completo de técnicas de motion para web, com implementação em Next.js e os casos em que cada uma compensa.
- motion
- scroll
Motions premium em Next.js: os 18 padrões do site da Plantica, passo a passo
O scroll vira CSS custom property e o CSS faz o resto: os 18 padrões de motion do site da Plantica em Next.js, com o prompt de implementação pronto de cada um.
- motion
- gsap


