Pular para o conteúdo
Zumkai

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
Card com o estado de suporte das scroll-driven animations nos três motores de browser em agosto de 2026.
Sumário
  1. Qual é o suporte real hoje
  2. scroll() ou view(): qual das duas você quer
  3. A armadilha que quebra sem avisar
  4. O que degrada sozinho e o que quebra feio
  5. O @supports que resolve
  6. Quando o IntersectionObserver continua sendo a resposta
  7. O que "compositor thread" muda na prática
  8. Receita completa: reveal que funciona nos três motores
  9. Quando não anima: a ordem de checagem
  10. Perguntas frequentes
  11. 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.

MotorEstadoDesde
Chrome / Edge (Blink)Enviado, sem flag115 — julho de 2023
Safari (WebKit)Enviado26 — setembro de 2025
Firefox (Gecko)Atrás de flagPadrão só no Nightly
Linha do tempo do suporte a scroll-driven animations por motor Blink enviou em julho de 2023, WebKit em setembro de 2025 com melhorias em 2026, e Gecko segue atrás de flag em agosto de 2026. 2023 2025 2026 Blink 115 · jul/2023 WebKit Safari 26 · set/2025 26.4 compositor Gecko atrás de flag · só Nightly
Linha cheia = enviado sem flag. Linha tracejada = implementado, desligado por padrão. Fontes: MDN, WebKit, Bugzilla Mozilla, acesso em 19/08/2026.

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.

css
.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.

css
.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:

css
.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:

NomeCobre
coverDa primeira borda entrando até a última saindo. É o intervalo completo
entrySó a entrada: da borda tocando a viewport até estar inteiramente dentro
exitSó a saída: de começar a sair até desaparecer
containO período em que o elemento está inteiramente visível
entry-crossingO 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.

css
/* 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?

EfeitoSem suportePrecisa de fallback?
Barra de progresso de leituraFica em 0%, invisível ou vaziaNão — é enfeite
Parallax de fundoFundo estáticoNão
Sombra ou cor que muda no scrollFica no estado inicialNão
Rotação ou escala decorativaFica no estado inicialNão
Reveal com opacity: 0Conteúdo invisível para sempreSim, obrigatório
Entrada com translate grandeConteúdo fora da telaSim, obrigatório
Item com visibility: hiddenConteúdo inacessívelSim, 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.

css
/* 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:

css
@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.

css
/* 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:

css
@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

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

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