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-polyfillpara 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.
- A verificação
- RESPEITE as preferências do usuário: algumas pessoas preferem menos movimento na Web. Use a consulta de mídia
prefers-reduced-motionpara 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
transformeopacity. Animar outras propriedades, comowidthouheight, pode causar problemas de desempenho. - USE a ordem de declaração correta: ao usar a propriedade abreviada
animation, declareanimation-timelineeanimation-rangedepois 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
blockpadrão (como em um scroller horizontal), seja explícito sobre qual eixo rastrear comview-timeline-axis. - VERIFIQUE se o escopo da pesquisa funciona: quando o elemento que declara o
view-timelinenão é um ancestral de árvore simples do elemento animado, eleve a visibilidade do nome doview-timelineusandotimeline-scopeem 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.