Conteúdo deste artigo
- O HTML
- Estilizando o contêiner pai
- Configurando um efeito de “máscara”
- Os layouts das colunas
- A animação!
- Concluindo
Às vezes, os designers têm ideias bobas que eventualmente crescem em você. Isso aconteceu comigo com esse conceito em que tive que construir colunas de itens movendo-se em direções opostas quando um usuário rola a página.
Nota: Esta demonstração respeita configurações de movimento reduzidas, então você precisará ativar o movimento para ver o efeito. E estamos analisando o suporte para Chrome e Safari enquanto escrevo isto.
Realmente não é tão difícil quanto você imagina, graças aos recursos CSS modernos, especificamente animações baseadas em rolagem . Não só isso, mas também é divertido de fazer! Deixe-me mostrar como abordei isso – e talvez você queira compartilhar como faria isso de forma diferente.
O HTML
O HTML consiste em um elemento pai (.opposing-columns), seus filhos (.opposing-column) e os filhos de seus filhos (.opposing-item):
<div class="opposing-columns">
<!-- Column 1 -->
<div class="opposing-column">
<div class="opposing-item">...</div>
<div class="opposing-item">...</div>
<div class="opposing-item">...</div>
</div>
<!-- Column 2 -->
<div class="opposing-column">
<div class="opposing-item">...</div>
<div class="opposing-item">...</div>
<div class="opposing-item">...</div>
</div>
<!-- Column 3 -->
<div class="opposing-column">
<div class="opposing-item">...</div>
<div class="opposing-item">...</div>
<div class="opposing-item">...</div>
</div>
</div>
Isso é tudo que precisamos na marcação. CSS fará o resto!
Estilizando o contêiner pai
Primeiro, vamos configurar as coisas para que esse efeito se aplique apenas a telas maiores — não faz sentido suportar algo assim em telas menores porque precisamos de espaço adicional para o efeito.
/* Just on larger screens */
@media screen and (width >= 50rem) {
.opposing-columns {
display: flex;
gap: 2rem;
max-inline-size: min(90dvi, 50rem);
margin-inline: auto;
}
}
Configurando um efeito de “máscara”
Precisamos fazer mais algumas coisas com o contêiner pai para ter a ilusão de que os itens em cada .opposing-column estão desaparecendo à medida que passam por ele. Os itens nas colunas externas movem-se para cima na rolagem e os itens na coluna central movem-se para baixo. À medida que ultrapassam os limites dos pais, queremos que desapareçam.
Então, vamos fazer algumas coisas. Primeiro, definiremos uma variável de cor de fundo no documento como um todo:
@media screen and (width >= 50rem) {
:root {
--opposing-bg: lightcyan;
background-color: var(--opposing-bg);
}
.opposing-columns {
/* same styles as before */
}
}
Em segundo lugar, aplicaremos a mesma cor de fundo nos pseudoelementos :before e :after do pai:
@media screen and (width >= 50rem) {
:root {
--opposing-bg: lightcyan;
background-color: var(--opposing-bg);
}
.opposing-columns {
/* same styles as before */
&:before,
&:after {
content: "";
position: absolute;
inset-inline: 0;
block-size: calc(var(--opposing-mask) * 3);
pointer-events: none;
z-index: 1;
}
}
}
Observe que estabelecemos um contexto de empilhamento nos pseudos e os colocamos uma camada acima do pai e de seus descendentes. Esta é a chave para mascarar os itens em cada coluna à medida que eles entram e saem do contêiner. Os itens estão tecnicamente deslizando sob as pseudomáscaras.
Falando nisso, vamos criar outra variável chamada --opposing-mask que adiciona espaço vertical entre o elemento pai e as três colunas:
@media screen and (width >= 50rem) {
:root {
--opposing-bg: lightcyan;
--opposing-mask: 3rem;
background-color: var(--opposing-bg);
}
.opposing-columns {
display: flex;
gap: 2rem;
max-inline-size: min(90dvi, 50rem);
margin-inline: auto;
margin-block: var(--opposing-mask, 3rem);
position: relative;
}
}
Vamos fazer a mesma coisa com os pseudos dos pais, aplicando apenas --opposing-mask ao seu block-size por um múltiplo de três. Dessa forma, há espaço vertical adicional entre eles e o pai.
@media screen and (width >= 50rem) {
:root {
--opposing-bg: lightcyan;
--opposing-mask: 3rem;
background-color: var(--opposing-bg);
}
.opposing-columns {
/* same styles as before */
&:before,
&:after {
content: "";
position: absolute;
inset-inline: 0;
block-size: calc(var(--opposing-mask) * 3);
pointer-events: none;
z-index: 1;
}
}
}
Você pode ver onde isso vai dar. Temos uma boa quantidade de espaço entre o contêiner pai e seus pseudos. Queremos que os itens da coluna apareçam como se estivessem desaparecendo à medida que rolam para fora do contêiner pai. Não precisamos mexer com sua opacidade ou algo parecido. Em vez disso, podemos adicionar gradientes de fundo nos pseudos.
O pseudo :before está na parte superior do contêiner, portanto, daremos a ele um gradiente que vai de uma cor sólida que corresponde à cor de fundo subjacente do documento até transparente, de cima para baixo. E como o pseudo :after fica na parte inferior do contêiner pai, inverteremos o gradiente para que fique transparente para a cor de fundo do documento, de baixo para cima.
@media screen and (width >= 50rem) {
:root {
/* same styles as before */
}
.opposing-columns {
/* same styles as before */
&:before,
&:after {
/* same styles as before */
}
&:before {
background-image: linear-gradient(
to bottom,
var(--opposing-bg) var(--opposing-mask),
transparent
);
inset-block-start: calc(var(--opposing-mask) * -1);
}
&:after {
background-image: linear-gradient(
to top,
var(--opposing-bg) var(--opposing-mask),
transparent
);
inset-block-end: calc(var(--opposing-mask) * -1);
}
}
}
}
Os layouts das colunas
Antes de entrarmos na mágica, devemos organizar os itens em cada coluna. Cada coluna é um item flexível dentro do pai, que é um contêiner flexível. Vamos deixá-los encolher (flex-shrink: 1) e crescer (flex-grow: 1), limitando o tamanho em um determinado ponto (flex-basis: 10rem).
Podemos definir tudo isso com a propriedade abreviada ###CODE_810ecdbcf3d2f5e69d2cf53b5988e337###:
@media screen and (width >= 50rem) {
/* same styles as before */
.opposing-column {
flex: 1 1 10rem;
}
}
Agora quero que essas colunas sejam contêineres de grade para que eu possa usar a propriedade gap para inserir espaço entre os itens:
@media screen and (width >= 50rem) {
/* same styles as before */
.opposing-column {
flex: 1 1 10rem;
display: grid;
gap: 2rem;
}
}
Poderíamos ter usado o Flexbox aqui também para obter acesso a gap, mas o layout padrão está definido como row e teríamos que substitua isso por column. Grid é um pouco mais conciso nesta situação.
A animação!
Foi para isso que você veio, certo? Configuramos tudo para que os itens da coluna possam entrar e sair do contêiner pai na rolagem. Agora precisamos adicionar esse comportamento de rolagem.
É aqui que a propriedade animation-timeline se torna realmente útil. Normalmente, uma animação CSS roda sozinha. Ele começa quando a página carrega (ou após um atraso específico definido) e termina após o tempo que você define a duração. Com animation-timeline, dizemos à animação para ser executada com base em sua posição de rolagem… daí o termo animação “acionada por rolagem”.
Temos duas funções suportadas aqui, scroll() e view() . Eles estão relacionados, mas são muito diferentes porque scroll() executa a animação com base na rolagem de um elemento position . A função view() é semelhante, mas rastreia o progresso do elemento conforme ele entra e sai do scrollport (ou seja, a área de rolagem do contêiner em que ele está).
Vamos com a função view() porque configuramos isso onde há uma área de rolagem clara dentro do contêiner pai. Precisamos executar a animação com base em onde ela entra e sai daquela área, e não na posição de rolagem dos itens da coluna.
Isso é muito interessante porque podemos dizer view() onde exatamente queremos que a animação comece quando ela entrar na área de rolagem e onde parar quando sair naquela mesma área. Assim:
/* Official syntax */
animation-timeline: view([ <axis> || <'view-timeline-inset'>]?);
Vamos começar definindo os eixos:
@media screen and (width >= 50rem) {
/* same styles as before */
.opposing-column {
/* ... */
animation-timeline: view();
animation-range: entry cover;
}
}
Isso é apenas parcialmente o que queremos, mas o que estamos dizendo é que queremos que a animação (1) comece no exato momento em que entrar na porta de rolagem (entry) e (2) termine quando sair completamente da área (cover). Precisamos ser explicitamente sobre as inserções porque é isso que estabelece o alcance da animação em relação ao local onde ela entra e sai. Queremos o intervalo completo, então entry começa em 0% e a saída é quando um item é covered em 100%.
@media screen and (width >= 50rem) {
/* same styles as before */
.opposing-column {
/* ... */
animation-timeline: view();
animation-range: entry 0% cover 100%;
}
}
Por fim, configuraremos a animação para ser executada linearmente — sem necessidade de os itens ficarem mais lentos ou mais lentos à medida que rolam.
@media screen and (width >= 50rem) {
/* same styles as before */
.opposing-column {
/* ... */
animation-timing-function: linear;
animation-timeline: view();
animation-range: entry 0% cover 100%;
}
}
OK, ótimo. Mas o que não fizemos foi criar uma animação. Configuramos o que queremos que ele faça quando for executado, mas precisamos definir o movimento real.
Quero configurar três animações CSS separadas:
- Aquele que traduz (move) os itens para cima na primeira coluna.
- Aquele que é o inverso da primeira animação para os itens da outra coluna.
Poderíamos tecnicamente definir a primeira animação em ambas as colunas externas, mas quero uma terceira que seja um pouco deslocada da primeira para que essas colunas pareçam escalonadas.
@keyframes scroll1 {
from { transform: translateY(var(--opposing-mask)); }
to { transform: translateY(calc(var(--opposing-mask) * -1)); }
}
@keyframes scroll2 {
from { transform: translateY(calc(var(--opposing-mask) * -1)); }
to { transform: translateY(var(--opposing-mask)); }
}
@keyframes scroll3 {
from { transform: translateY(calc(var(--opposing-mask) * .66)); }
to { transform: translateY(calc(var(--opposing-mask) * -.33)); }
}
Podemos criar variáveis para eles, é claro, caso precisemos atualizá-los:
@media screen and (width >= 50rem) {
:root {
--opposing-bg: lightcyan;
--opposing-mask: 3rem;
--animation-1: scroll1;
--animation-2: scroll2;
--animation-3: scroll3;
/* ... */
}
}
…e aplique-os a cada coluna:
@media screen and (width >= 50rem) {
/* same styles as before */
.opposing-column {
/* same styles as before */
}
:where(.opposing-column:nth-of-type(1)) {
animation-name: var(--animation-1);
}
:where(.opposing-column:nth-of-type(2)) {
animation-name: var(--animation-2);
}
:where(.opposing-column:nth-of-type(3)) {
animation-name: var(--animation-3);
}
}
Já que estamos nisso, devemos desabilitar as animações para respeitar as configurações do usuário para movimento reduzido (e remover a máscara, caso contrário pode parecer estranho):
@media (prefers-reduced-motion: reduce) {
.opposing-column {
animation: unset;
&:before,
&:after {
content: unset;
}
}
}
Concluindo
Sim, animações baseadas em rolagem são muito, muito legais. Ainda estamos aguardando o suporte do Firefox enquanto escrevo isso, mas você certamente pode agrupar isso em @supports para fornecer uma experiência padrão que usa as anotações de rolagem e, em seguida, definir uma experiência alternativa para navegadores não compatíveis, como executar em uma linha do tempo de animação normal:
@supports (animation-timeline: view()) {
/* ... */
}
Isso é apenas uma introdução ao que as animações baseadas em rolagem podem fazer, é claro. Que tipo de coisas você fez ou experimentou? Ou você abordaria este de forma diferente? Avise!
Usando animações orientadas por rolagem para direções de rolagem opostas originalmente escrito à mão e publicado com amor em CSS-Tricks . Você realmente deveria receber o boletim informativo também.
