Scrollytelling

O scrollytelling é uma técnica conhecida usada para criar experiências imersivas e envolventes na Web. Envolve a animação de elementos em uma página conforme o usuário rola a tela, contando uma história ou guiando o usuário por uma narrativa. Com as animações de rolagem em CSS, é possível criar esses efeitos diretamente em CSS, sem precisar do JavaScript. As animações são controladas pela posição de rolagem, não por um relógio baseado em tempo, o que garante que elas estejam sempre sincronizadas com a rolagem do usuário.

Como fazer a implementação

Para criar uma experiência de scrollytelling, você precisa de dois conjuntos de elementos: um para acompanhar a posição de rolagem e outro para ser animado.

Primeiro, defina um view-timeline nomeado nos elementos que você quer rastrear. Eles vão servir como os drivers das suas animações.

#tracked {
  section:nth-child(1){ view-timeline: --tl-1 block; }
  section:nth-child(2){ view-timeline: --tl-2 block; }
  section:nth-child(3){ view-timeline: --tl-3 block; }
  section:nth-child(4){ view-timeline: --tl-4 block; }
  section:nth-child(5){ view-timeline: --tl-5 block; }
}

Em seguida, aplique animações aos elementos que você quer animar e vincule-os às linhas do tempo que acabou de criar usando a propriedade animation-timeline.

#animated {
  section {
    animation: animate-in auto linear both, animate-out auto linear forwards;
    animation-range: entry 25% cover 50%, exit 50% exit 75%;
  }

  section:nth-child(1){ animation-timeline: --tl-1; }
  section:nth-child(2){ animation-timeline: --tl-2; }
  section:nth-child(3){ animation-timeline: --tl-3; }
  section:nth-child(4){ animation-timeline: --tl-4; }
  section:nth-child(5){ animation-timeline: --tl-5; }
}

Para que o animation-timeline possa referenciar as linhas do tempo nomeadas, elas precisam estar no mesmo escopo. Use a propriedade timeline-scope em um ancestral comum para disponibilizar as linhas do tempo para todos os elementos que precisam delas. O elemento :root costuma ser uma boa opção para isso.

html {
  timeline-scope: --tl-1, --tl-2, --tl-3, --tl-4, --tl-5;
}

Por fim, use a propriedade animation-range para especificar o intervalo exato da linha do tempo em que a animação deve ser executada. Isso oferece um controle refinado sobre quando as animações são acionadas e como elas progridem.

#animated section {
  animation-range: entry 25% cover 50%, exit 50% exit 75%;
}

Exemplo de código

html {
  timeline-scope: --tl-1, --tl-2, --tl-3, --tl-4, --tl-5;
}

#tracked {
  section:nth-child(1){ view-timeline: --tl-1 block; }
  section:nth-child(2){ view-timeline: --tl-2 block; }
  section:nth-child(3){ view-timeline: --tl-3 block; }
  section:nth-child(4){ view-timeline: --tl-4 block; }
  section:nth-child(5){ view-timeline: --tl-5 block; }
}

@keyframes animate-in {
  from { scale: 0.5; opacity: 0; transform: rotateY(-180deg); }
  to { transform: rotateY(0deg); }
}
@keyframes animate-out {
  to { translate: 100% 0; opacity: 0; }
}

#animated {
  section {
    animation: animate-in auto linear both, animate-out auto linear forwards;
    animation-range: entry 25% cover 50%, exit 50% exit 75%;
    backface-visibility: hidden;
  }

  section:nth-child(1){ animation-timeline: --tl-1; }
  section:nth-child(2){ animation-timeline: --tl-2; }
  section:nth-child(3){ animation-timeline: --tl-3; }
  section:nth-child(4){ animation-timeline: --tl-4; }
  section:nth-child(5){ animation-timeline: --tl-5; }
}

/* MANDATORY Copy-Paste Safety: Disable continuous storytelling motion for sensitive users */
@media (prefers-reduced-motion: reduce) {
  #animated section {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
}

Práticas recomendadas

Ao usar animações acionadas por rolagem, é importante seguir algumas práticas recomendadas para garantir uma experiência tranquila e acessível:

  • FAÇA a detecção de recursos: nem todos os navegadores oferecem suporte a animações acionadas por rolagem. Use @supports ((animation-timeline: scroll()) and (animation-range: 0% 100%)) para verificar a compatibilidade e fornecer um substituto para navegadores que não oferecem suporte.
    • A verificação (animation-range: 0% 100%) PRECISA ser incluída aqui para filtrar navegadores com suporte parcial.
    • NÃO use o pacote scroll-timeline-polyfill para a estratégia de substituição, porque ele não tem todos os recursos e tem muitos problemas conhecidos.
    • Se a animação for considerada apenas decorativa, opte pelo aprimoramento progressivo e NÃO forneça um substituto.
  • RESPEITE as preferências do usuário: algumas pessoas preferem menos movimento na Web. Use a consulta de mídia prefers-reduced-motion para desativar ou reduzir as animações para esses usuários.
  • FAÇA o possível para animar apenas propriedades CSS de alto desempenho: para animações mais suaves, anime propriedades que podem ser processadas pela linha de execução do compositor do navegador, como transform e opacity. Animar outras propriedades, como width ou height, pode causar problemas de desempenho.
  • USE a ordem de declaração correta: ao usar a propriedade abreviada animation, declare animation-timeline e animation-range depois dela para evitar que a abreviação redefina a linha do tempo.

Ao usar a propriedade view-timeline para criar uma animação acionada por rolagem:

  • USE um CSS <dashed-ident> para o nome.
  • OPCIONAL: seja explícito sobre o eixo a ser rastreado. Quando não estiver segmentando o eixo block padrão (como em um scroller horizontal), seja explícito sobre qual eixo rastrear com view-timeline-axis.
  • VERIFIQUE se o escopo da pesquisa funciona: quando o elemento que declara o view-timeline não é um ancestral de árvore simples do elemento animado, eleve a visibilidade do nome do view-timeline usando timeline-scope em um ancestral compartilhado.

Estratégias de fallback

Para navegadores que não oferecem suporte a animações acionadas por rolagem, use um substituto para recriar os efeitos visuais. Os substitutos geralmente são criados com um listener de rolagem (para efeitos de ScrollTimeline) ou com a API IntersectionObserver (para efeitos de ViewTimeline).

Em navegadores com suporte integrado para animações de rolagem, use SEMPRE a implementação nativa de CSS, porque ela tem melhor desempenho.

Nem todos os efeitos podem ser recriados usando a abordagem de substitutos.

Para esse caso de uso específico, o script a seguir aplica o substituto para navegadores que não oferecem suporte a animações de rolagem. Ele usa um IntersectionObserver para rastrear a visibilidade de cada elemento #tracked section e atualiza a propriedade transform do #animated section correspondente.

const animatedSections = document.querySelectorAll('#animated section');

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    const sectionIndex = Array.from(document.querySelectorAll('#tracked section')).indexOf(entry.target);
    if (sectionIndex !== -1) {
      const animatedSection = animatedSections[sectionIndex];
      const ratio = entry.intersectionRatio;

      // Animate-in
      animatedSection.style.opacity = ratio;
      animatedSection.style.transform = `scale(${0.5 + ratio * 0.5}) rotateY(${-180 + ratio * 180}deg)`;

      // Animate-out
      if (ratio < 0.5) {
        animatedSection.style.translate = `${(0.5 - ratio) * 2 * 100}% 0`;
      } else {
        animatedSection.style.translate = '0 0';
      }
    }
  });
}, { threshold: Array.from({length: 101}, (_, i) => i / 100) });

document.querySelectorAll('#tracked section').forEach(section => {
  observer.observe(section);
});

E o CSS que acompanha:

#animated section {
  opacity: 0;
  transform: scale(0.5)  rotateY(-180deg);
  backface-visibility: hidden;
}

/* MANDATORY Copy-Paste Safety: Ensure content remains fully visible and legible for assistive technologies or users with motion sensitivities */
@media (prefers-reduced-motion: reduce) {
  #animated section {
    opacity: 1 !important;
    transform: none !important;
    translate: 0 0 !important;
  }
}

Esse substituto oferece uma animação mais precisa e baseada em rolagem para navegadores que não oferecem suporte ao recurso CSS nativo, garantindo uma experiência mais consistente para todos os usuários. Ao usar uma série de limites para o IntersectionObserver, podemos acompanhar a posição de rolagem com mais precisão e criar uma animação mais suave.