Aprenda a trabalhar com linhas do tempo de rolagem e de visualização para criar animações de rolagem de maneira declarativa.
Publicado em 5 de maio de 2023
Animações de rolagem
As animações de rolagem são um padrão de UX comum na Web. Uma animação de rolagem está vinculada à posição de rolagem de um contêiner de rolagem. Isso significa que, conforme você rola para cima ou para baixo, a animação vinculada avança ou retrocede em resposta direta. Exemplos disso são efeitos como imagens de fundo com paralaxe ou indicadores de leitura que se movem conforme você rola a tela.
Um tipo semelhante de animação de rolagem é uma animação vinculada à posição de um elemento dentro do contêiner de rolagem. Com ele, por exemplo, os elementos podem aparecer gradualmente à medida que aparecem.
A maneira clássica de conseguir esses tipos de efeitos é responder a eventos de rolagem na linha de execução principal, o que leva a dois problemas principais:
- Os navegadores modernos executam a rolagem em um processo separado e, portanto, enviam eventos de rolagem de forma assíncrona.
- As animações da linha de execução principal estão sujeitas a travamentos.
Isso torna impossível ou muito difícil criar animações de rolagem eficientes que estejam sincronizadas com a rolagem.
A partir da versão 115 do Chrome, há um novo conjunto de APIs e conceitos que podem ser usados para ativar animações declarativas movidas por rolagem: linhas do tempo de rolagem e de visualização.
Esses novos conceitos se integram à API Web Animations (WAAPI) e à API CSS Animations, permitindo que eles herdem as vantagens dessas APIs. Isso inclui a capacidade de executar animações com rolagem na linha de execução principal. Sim, você leu certo: agora é possível ter animações suaves, controladas por rolagem, executadas fora da linha de execução principal, com apenas algumas linhas de código extra. O que não gostar?!
Animações na Web: uma pequena recapitulação
Animações na Web com CSS
Para criar uma animação em CSS, defina um conjunto de frames-chave usando a regra @keyframes
. Vincule-o a um elemento usando a propriedade animation-name
e defina um animation-duration
para determinar a duração da animação. Há mais propriedades longhand animation-*
disponíveis, como animation-easing-function
e animation-fill-mode
, para citar apenas algumas, que podem ser combinadas na abreviação animation
.
Por exemplo, esta é uma animação que aumenta o tamanho de um elemento no eixo X e muda a cor do plano de fundo:
@keyframes scale-up {
from {
background-color: red;
transform: scaleX(0);
}
to {
background-color: darkred;
transform: scaleX(1);
}
}
#progressbar {
animation: 2.5s linear forwards scale-up;
}
.
Animações na Web com JavaScript
Em JavaScript, a API Web Animations pode ser usada para fazer exatamente a mesma coisa. Para isso, crie novas instâncias Animation
e KeyFrameEffect
ou use o método Element
animate()
, muito mais curto.
document.querySelector('#progressbar').animate(
{
backgroundColor: ['red', 'darkred'],
transform: ['scaleX(0)', 'scaleX(1)'],
},
{
duration: 2500,
fill: 'forwards',
easing: 'linear',
}
);
O resultado visual do snippet de JavaScript acima é idêntico à versão anterior do CSS.
Linhas do tempo de animação
Por padrão, uma animação anexada a um elemento é executada na linha do tempo do documento. O tempo de origem começa em 0 quando a página é carregada e começa a avançar conforme o tempo avança. Esta é a linha do tempo de animação padrão e, até agora, era a única à qual você tinha acesso.
A especificação de animações de rolagem define dois novos tipos de linhas do tempo que podem ser usados:
- Linha do tempo do progresso de rolagem: uma linha do tempo vinculada à posição de rolagem de um contêiner de rolagem ao longo de um eixo específico.
- Linha do tempo do progresso de visualização: uma linha do tempo vinculada à posição relativa de um elemento específico no contêiner de rolagem.
Rolar a linha do tempo do progresso
Uma linha do tempo do progresso de rolagem é uma linha do tempo de animação vinculada ao progresso na posição de rolagem de um contêiner de rolagem, também chamado de janela ou botão de rolagem, ao longo de um eixo específico. Ele converte uma posição de um período de rolagem em uma porcentagem de progresso.
A posição de rolagem inicial representa 0% de progresso, e a final representa 100% de progresso. Na visualização a seguir, é possível ver que o progresso aumenta de 0% a 100% à medida que você rola o botão de cima para baixo.
✨ Teste por conta própria
Uma linha do tempo do progresso de rolagem é frequentemente abreviada para "linha do tempo de rolagem".
Linha do tempo do progresso de visualização
Esse tipo de linha do tempo está vinculado ao progresso relativo de um elemento específico dentro de um contêiner de rolagem. Assim como uma linha do tempo do progresso de rolagem, o deslocamento de rolagem de um botão é rastreado. Ao contrário de uma linha do tempo do progresso de rolagem, essa é a posição relativa de um objeto dentro desse botão que determina o progresso.
Isso é semelhante ao funcionamento do IntersectionObserver
, que pode rastrear o quanto um elemento está visível na rolagem. Se o elemento não estiver visível no botão, não há interseção. Caso esteja visível, mesmo para a menor parte, então há interseção.
A linha do tempo do progresso de visualização se inicia no momento em que um objeto começa a cruzar com o botão de rolagem e termina quando ele para de cruzar com o botão. Na visualização a seguir, é possível ver que o progresso começa a contar a partir de 0% quando o objeto entra no contêiner de rolagem e atinge 100% no momento em que sai dele.
✨ Teste por conta própria
Uma linha do tempo do progresso de visualização é abreviada como "linha do tempo de visualização". É possível segmentar partes específicas de uma linha do tempo de visualização com base no tamanho do assunto, mas falaremos mais sobre isso mais tarde.
Como usar as linhas do tempo do progresso de rolagem
Como criar uma linha do tempo de progresso de rolagem anônima no CSS
A maneira mais fácil de criar uma linha do tempo de rolagem no CSS é usando a função scroll()
. Isso cria uma linha do tempo de rolagem anônima que pode ser definida como o valor da nova propriedade animation-timeline
.
Exemplo:
@keyframes animate-it { … }
.subject {
animation: animate-it linear;
animation-timeline: scroll(root block);
}
A função scroll()
aceita um <scroller>
e um argumento <axis>
.
Os valores aceitos para o argumento <scroller>
são:
nearest
: usa o contêiner de rolagem ancestral mais próximo (padrão).root
: usa a janela de visualização do documento como o contêiner de rolagem.self
: usa o próprio elemento como o contêiner de rolagem.
Os valores aceitos para o argumento <axis>
são:
block
: usa a medida de progresso ao longo do eixo do bloco do contêiner de rolagem (padrão).inline
: usa a medida de progresso ao longo do eixo inline do contêiner de rolagem.y
: usa a medida de progresso ao longo do eixo y do contêiner de rolagem.x
: usa a medida de progresso ao longo do eixo x do contêiner de rolagem.
Por exemplo, para vincular uma animação ao botão de rolagem raiz no eixo do bloco, os valores a serem transmitidos para scroll()
são root
e block
. O valor é scroll(root block)
.
Demonstração: indicador de progresso de leitura
Esta demonstração tem um indicador de progresso de leitura fixado na parte de cima da viewport. Conforme você rola a página para baixo, a barra de progresso cresce até ocupar toda a largura da janela de visualização ao chegar ao final do documento. Uma linha do tempo de progresso de rolagem anônima é usada para direcionar a animação.
✨ Teste por conta própria
O indicador de progresso da leitura é posicionado na parte de cima da página usando a posição fixa. Para aproveitar animações compostas, não o width
é animado, mas o elemento é reduzido no eixo x usando um transform
.
<body>
<div id="progress"></div>
…
</body>
@keyframes grow-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
#progress {
position: fixed;
left: 0; top: 0;
width: 100%; height: 1em;
background: red;
transform-origin: 0 50%;
animation: grow-progress auto linear;
animation-timeline: scroll();
}
A linha do tempo da animação grow-progress
no elemento #progress
é definida como uma linha do tempo anônima criada usando scroll()
. Nenhum argumento é fornecido para scroll()
, então ele vai voltar aos valores padrão.
O controle deslizante padrão a ser rastreado é o nearest
, e o eixo padrão é block
. Isso afeta o controle de rolagem raiz, que é o controle de rolagem mais próximo do elemento #progress
, enquanto rastreia a direção do bloco.
Como criar uma linha do tempo de progresso de rolagem nomeada no CSS
Uma forma alternativa de definir uma linha do tempo do progresso de rolagem é usar uma já nomeada. É um pouco mais detalhado, mas pode ser útil quando você não está direcionando um botão de rolagem principal ou o botão de rolagem raiz, ou quando a página usa várias linhas do tempo ou quando as pesquisas automáticas não funcionam. Dessa forma, você pode identificar uma linha do tempo do progresso de rolagem pelo nome que você deu a ela.
Para criar uma linha do tempo do progresso de rolagem nomeada em um elemento, defina a propriedade CSS scroll-timeline-name
no contêiner de rolagem com um identificador de sua preferência. O valor precisa começar com --
.
Para ajustar o eixo a ser rastreado, declare também a propriedade scroll-timeline-axis
. Os valores permitidos são os mesmos do argumento <axis>
de scroll()
.
Por fim, para vincular a animação à linha do tempo do progresso da rolagem, defina a propriedade animation-timeline
no elemento que precisa ser animado com o mesmo valor do identificador usado para o scroll-timeline-name
.
Exemplo de código:
@keyframes animate-it { … }
.scroller {
scroll-timeline-name: --my-scroller;
scroll-timeline-axis: inline;
}
.scroller .subject {
animation: animate-it linear;
animation-timeline: --my-scroller;
}
Se quiser, você pode combinar scroll-timeline-name
e scroll-timeline-axis
na abreviação scroll-timeline
. Exemplo:
scroll-timeline: --my-scroller inline;
Demonstração: indicador de etapa do carrossel horizontal
Esta demonstração tem um indicador de etapa mostrado acima de cada carrossel de imagens. Quando um carrossel tem três imagens, a barra de indicador começa com 33% de largura para indicar que você está visualizando a primeira imagem. Quando a última imagem aparece, determinada pela rolagem até o fim, o indicador ocupa toda a largura do controle. Uma linha do tempo de progresso de rolagem nomeada é usada para direcionar a animação.
✨ Teste por conta própria
O markup básico de uma galeria é este:
<div class="gallery" style="--num-images: 2;">
<div class="gallery__scrollcontainer">
<div class="gallery__progress"></div>
<div class="gallery__entry">…</div>
<div class="gallery__entry">…</div>
</div>
</div>
O elemento .gallery__progress
é posicionado de forma absoluta dentro do elemento de wrapper .gallery
. O tamanho inicial é determinado pela propriedade personalizada --num-images
.
.gallery {
position: relative;
}
.gallery__progress {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 1em;
transform: scaleX(calc(1 / var(--num-images)));
}
O .gallery__scrollcontainer
exibe os elementos .gallery__entry
contidos horizontalmente e é o elemento que rola. Ao rastrear a posição de rolagem, o .gallery__progress
é animado. Isso é feito referindo-se à linha do tempo do progresso de rolagem nomeada --gallery__scrollcontainer
.
@keyframes grow-progress {
to { transform: scaleX(1); }
}
.gallery__scrollcontainer {
overflow-x: scroll;
scroll-timeline: --gallery__scrollcontainer inline;
}
.gallery__progress {
animation: auto grow-progress linear forwards;
animation-timeline: --gallery__scrollcontainer;
}
Como criar uma linha do tempo de progresso de rolagem com JavaScript
Para criar uma linha do tempo de rolagem em JavaScript, crie uma nova instância da classe ScrollTimeline
. Transmita um pacote de propriedade com o source
e o axis
que você quer rastrear.
source
: uma referência ao elemento cujo controle deslizante você quer rastrear. Usedocument.documentElement
para segmentar o scroller raiz.axis
: determina qual eixo rastrear. Assim como na variante CSS, os valores aceitos sãoblock
,inline
,x
ey
.
const tl = new ScrollTimeline({
source: document.documentElement,
});
Para anexar a uma animação da Web, transmita como a propriedade timeline
e omita qualquer duration
, se houver.
$el.animate({
opacity: [0, 1],
}, {
timeline: tl,
});
Demonstração: indicador de progresso de leitura, revisitado
Para recriar o indicador de progresso da leitura com JavaScript usando a mesma marcação, use o seguinte código JavaScript:
const $progressbar = document.querySelector('#progress');
$progressbar.style.transformOrigin = '0% 50%';
$progressbar.animate(
{
transform: ['scaleX(0)', 'scaleX(1)'],
},
{
fill: 'forwards',
timeline: new ScrollTimeline({
source: document.documentElement,
}),
}
);
O resultado visual é idêntico na versão CSS: o timeline
criado rastreia o scroller raiz e dimensiona o #progress
no eixo x de 0% a 100% à medida que você rola a página.
✨ Teste por conta própria
Como usar a linha do tempo do progresso de visualização
Como criar uma linha do tempo do progresso de visualização anônima no CSS
Para criar uma linha do tempo do progresso de visualização, use a função view()
. Os argumentos aceitos são <axis>
e <view-timeline-inset>
.
- O
<axis>
é o mesmo da linha do tempo do progresso de rolagem e define qual eixo rastrear. O valor padrão éblock
. - Com
<view-timeline-inset>
, é possível especificar um deslocamento (positivo ou negativo) para ajustar os limites quando um elemento é considerado visível ou não. O valor precisa ser uma porcentagem ouauto
, sendoauto
o valor padrão.
Por exemplo, para vincular uma animação a um elemento que se cruza com o botão de rolagem no eixo do bloco, use view(block)
. De forma semelhante a scroll()
, defina esse valor como a propriedade animation-timeline
e não se esqueça de definir animation-duration
como auto
.
Usando o código abaixo, cada img
vai aparecer gradualmente à medida que cruza a viewport enquanto você rola a tela.
@keyframes reveal {
from { opacity: 0; }
to { opacity: 1; }
}
img {
animation: reveal linear;
animation-timeline: view();
}
Interlúdio: conferir os períodos da linha do tempo
Por padrão, uma animação vinculada à linha do tempo de visualização é anexada a todo o período dela. Isso começa a partir do momento em que o objeto está prestes a entrar na janela de rolagem e termina quando ele sai dela.
Também é possível vincular a uma parte específica da linha do tempo de visualização especificando o período ao qual ele deve ser anexado. Por exemplo, isso pode acontecer apenas quando o objeto estiver entrando no botão de rolagem. Na visualização a seguir, o progresso começa a contar a partir de 0%, quando o objeto entra no contêiner de rolagem, mas chega logo a 100% a partir do momento em que está totalmente intersectado.
Os possíveis períodos da linha do tempo de visualização que você pode segmentar são os seguintes:
cover
: representa o período completo da linha do tempo do progresso da visualização.entry
: representa o período durante o qual a caixa principal está entrando no período de visibilidade do progresso da visualização.exit
: representa o período durante o qual a caixa principal está saindo do período de visibilidade do progresso da visualização.entry-crossing
: representa o período durante o qual a caixa principal cruza a borda final.exit-crossing
: representa o período durante o qual a caixa principal cruza a borda inicial.contain
: representa o período durante o qual a caixa principal é totalmente contida ou totalmente coberta por seu período de visibilidade do progresso da visualização dentro da janela de rolagem. Isso depende se o assunto está mais alto ou mais baixo do que o botão de rolagem.
Para definir um intervalo, você precisa definir um início e um fim. Cada um consiste em um nome de intervalo (consulte a lista acima) e um deslocamento de intervalo para determinar a posição dentro desse nome. O deslocamento de intervalo geralmente é uma porcentagem que varia de 0%
a 100%
, mas você também pode especificar um comprimento fixo, como 20em
.
Por exemplo, se você quiser executar uma animação a partir do momento em que um sujeito entra, escolha entry 0%
como o início do intervalo. Para que ele seja concluído até a entrada do sujeito, escolha entry 100%
como valor para o fim do intervalo.
No CSS, você define isso usando a propriedade animation-range
. Exemplo:
animation-range: entry 0% entry 100%;
Em JavaScript, use as propriedades rangeStart
e rangeEnd
.
$el.animate(
keyframes,
{
timeline: tl,
rangeStart: 'entry 0%',
rangeEnd: 'entry 100%',
}
);
Use a ferramenta incorporada abaixo para saber o que cada nome de intervalo representa e como as porcentagens afetam as posições inicial e final. Tente definir o início do intervalo como entry 0%
e o fim do intervalo como cover 50%
e arraste a barra de rolagem para conferir o resultado da animação.
Assistir uma gravação
Ao usar as ferramentas de intervalos da visualização da linha do tempo, você pode notar que alguns intervalos podem ser segmentados por duas combinações diferentes de nome de intervalo + deslocamento de intervalo. Por exemplo, entry 0%
, entry-crossing 0%
e cover 0%
são direcionados para a mesma área.
Quando o início e o fim do intervalo têm o mesmo nome e abrangem todo o intervalo (de 0% a 100%), você pode encurtar o valor para apenas o nome do intervalo. Por exemplo, animation-range: entry 0% entry 100%;
pode ser reescrito para o animation-range: entry
muito mais curto.
Demonstração: revelação de imagem
Esta demonstração mostra as imagens em degradê conforme elas entram na janela de rolagem. Isso é feito usando uma linha do tempo de visualização anônima. O intervalo de animação foi ajustado para que cada imagem fique com opacidade total quando estiver na metade do controle deslizante.
✨ Teste por conta própria
O efeito de expansão é alcançado usando um clip-path animado. O CSS usado para esse efeito é este:
@keyframes reveal {
from { opacity: 0; clip-path: inset(0% 60% 0% 50%); }
to { opacity: 1; clip-path: inset(0% 0% 0% 0%); }
}
.revealing-image {
animation: auto linear reveal both;
animation-timeline: view();
animation-range: entry 25% cover 50%;
}
Como criar uma linha do tempo do progresso de visualização nomeada no CSS
Assim como as linhas do tempo de rolagem têm versões nomeadas, você também pode criar linhas do tempo de visualização nomeadas. Em vez das propriedades scroll-timeline-*
, use variantes que tenham o prefixo view-timeline-
, como view-timeline-name
e view-timeline-axis
.
O mesmo tipo de valores e as mesmas regras para pesquisar uma linha do tempo com nome são aplicados.
Demonstração: revelação de imagem, revisitada
Retrabalhando a demonstração de revelação de imagem anterior, o código revisado é este:
.revealing-image {
view-timeline-name: --revealing-image;
view-timeline-axis: block;
animation: auto linear reveal both;
animation-timeline: --revealing-image;
animation-range: entry 25% cover 50%;
}
Ao usar view-timeline-name: revealing-image
, o elemento será rastreado no scroller mais próximo. O mesmo valor é usado como o valor da propriedade animation-timeline
. A saída visual é exatamente a mesma de antes.
✨ Teste por conta própria
Como criar uma linha do tempo do progresso de visualização em JavaScript
Para criar uma linha do tempo de visualização no JavaScript, crie uma nova instância da classe ViewTimeline
. Transmita um pacote de propriedade com o subject
que você quer rastrear, axis
e inset
.
subject
: uma referência ao elemento que você quer rastrear no próprio scroller.axis
: o eixo a ser rastreado. Assim como na variante CSS, os valores aceitos sãoblock
,inline
,x
ey
.inset
: um ajuste de inserção (positivo) ou de saída (negativo) da janela de rolagem ao determinar se a caixa está visível.
const tl = new ViewTimeline({
subject: document.getElementById('subject'),
});
Para anexar a uma animação da Web, transmita como a propriedade timeline
e omita qualquer duration
, se houver. Se preferir, transmita as informações do intervalo usando as propriedades rangeStart
e rangeEnd
.
$el.animate({
opacity: [0, 1],
}, {
timeline: tl,
rangeStart: 'entry 25%',
rangeEnd: 'cover 50%',
});
✨ Teste por conta própria
Mais coisas para testar
Como anexar a vários intervalos da linha do tempo de visualização com um conjunto de frames-chave
Vamos conferir esta demonstração de lista de contatos em que as entradas da lista são animadas. Quando uma entrada de lista entra na janela de rolagem pela parte de baixo, ela desliza e desbota. Quando ela sai da janela de rolagem pela parte de cima, ela desliza e desbota.
✨ Teste por conta própria
Para esta demonstração, cada elemento é decorado com uma linha do tempo de visualização que rastreia o elemento à medida que ele atravessa o scrollport, mas duas animações movidas por rolagem são anexadas a ele. A animação animate-in
é anexada ao intervalo entry
da linha do tempo, e a animação animate-out
ao intervalo exit
da linha do tempo.
@keyframes animate-in {
0% { opacity: 0; transform: translateY(100%); }
100% { opacity: 1; transform: translateY(0); }
}
@keyframes animate-out {
0% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(-100%); }
}
#list-view li {
animation: animate-in linear forwards,
animate-out linear forwards;
animation-timeline: view();
animation-range: entry, exit;
}
Em vez de executar duas animações diferentes anexadas a dois intervalos diferentes, também é possível criar um conjunto de frames-chave que já contenha as informações do intervalo.
@keyframes animate-in-and-out {
entry 0% {
opacity: 0; transform: translateY(100%);
}
entry 100% {
opacity: 1; transform: translateY(0);
}
exit 0% {
opacity: 1; transform: translateY(0);
}
exit 100% {
opacity: 0; transform: translateY(-100%);
}
}
#list-view li {
animation: linear animate-in-and-out;
animation-timeline: view();
}
Como os frames-chave contêm as informações de período, não é necessário especificar o animation-range
. O resultado é exatamente o mesmo de antes.
✨ Teste por conta própria
Como anexar a uma linha do tempo de rolagem que não é ancestral
O mecanismo de pesquisa para linhas do tempo de rolagem e de visualização nomeadas é limitado apenas aos ancestrais de rolagem. No entanto, muitas vezes, o elemento que precisa ser animado não é filho do scroller que precisa ser rastreado.
Para que isso funcione, a propriedade timeline-scope
entra em ação. Use essa propriedade para declarar uma linha do tempo com esse nome sem realmente criá-la. Isso dá à linha do tempo com esse nome um escopo mais amplo. Na prática, você usa a propriedade timeline-scope
em um elemento pai compartilhado para que a linha do tempo de um scroller filho possa ser anexada a ele.
Exemplo:
.parent {
timeline-scope: --tl;
}
.parent .scroller {
scroll-timeline: --tl;
}
.parent .scroller ~ .subject {
animation: animate linear;
animation-timeline: --tl;
}
Neste snippet:
- O elemento
.parent
declara uma linha do tempo com o nome--tl
. Qualquer elemento filho pode encontrar e usar esse valor como um valor para a propriedadeanimation-timeline
. - O elemento
.scroller
define uma linha do tempo de rolagem com o nome--tl
. Por padrão, ele só seria visível para os filhos, mas como.parent
está definido comoscroll-timeline-root
, ele é anexado a ele. - O elemento
.subject
usa a linha do tempo--tl
. Ele percorre a árvore de ancestrais e encontra--tl
no.parent
. Com o--tl
no.parent
apontando para o--tl
do.scroller
, o.subject
vai rastrear a linha do tempo do progresso de rolagem do.scroller
.
Em outras palavras, você pode usar timeline-root
para mover uma linha do tempo até um ancestral (também conhecido como elevação), para que todas as crianças do ancestral possam acessá-la.
A propriedade timeline-scope
pode ser usada com linhas do tempo de rolagem e de visualização.
Mais demonstrações e recursos
Todas as demonstrações abordadas neste artigo no minisite scroll-driven-animations.style. O site inclui muitas outras demonstrações para destacar o que é possível com animações baseadas em rolagem.
Uma das outras demos é esta lista de capas de álbuns. Cada capa gira em 3D enquanto ocupa o centro das atenções.
✨ Teste por conta própria
Ou esta demonstração de cards empilhados que usa position: sticky
. À medida que os cards são empilhados, os que já estão presos são reduzidos, criando um efeito de profundidade legal. No final, a pilha inteira desliza para fora da tela como um grupo.
✨ Teste por conta própria
Também incluído em scroll-driven-animations.style, há uma coleção de ferramentas, como a visualização do progresso do intervalo da linha do tempo de visualização, que foi incluída anteriormente nesta postagem.
As animações de rolagem também são abordadas em Novidades sobre animações na Web no Google I/O 2023.