As barras de rolagem personalizadas são extremamente raras, principalmente porque elas são um dos poucos elementos da Web que não podem ser estilados (estou falando de você, seletor de datas). Você pode usar o JavaScript para criar o seu, mas isso é caro, de baixa fidelidade e pode parecer lento. Neste artigo, vamos aproveitar algumas matrizes CSS não convencionais para criar um scroller personalizado que não requer JavaScript durante a rolagem, apenas um código de configuração.
Texto longo, leia o resumo
Você não se importa com as pequenas coisas? Você só quer conferir a demonstração do Nyan Cat e acessar a biblioteca? Encontre o código da demonstração no nosso repositório do GitHub.
LAM;WRA (Long and mathematical; will read anyways)
Há algum tempo, criamos um scroller de paralaxe. Você leu este artigo? É muito bom, vale a pena o tempo gasto). Ao empurrar elementos para trás usando transformações 3D do CSS, os elementos se movem mais lentamente do que a velocidade de rolagem real.
Recapitulação
Vamos começar com uma recapitulação de como o scroller de paralaxe funciona.
Como mostrado na animação, conseguimos o efeito de paralaxe empurrando elementos "para trás" no espaço 3D, ao longo do eixo Z. Rolar um documento é uma translação ao longo do eixo Y. Portanto, se rolarmos para baixo, digamos, 100 px, cada elemento será movido para cima em 100 px. Isso se aplica a todos os elementos, mesmo aqueles que estão "mais atrás". Mas como eles estão mais distantes da câmera, o movimento observado na tela será menor que 100 px, gerando o efeito de paralaxe desejado.
É claro que mover um elemento para trás no espaço também vai fazer com que ele pareça menor, o que podemos corrigir redimensionando o elemento. Descobrimos a matemática exata quando criamos o controle de paralaxe, então não vou repetir todos os detalhes.
Etapa 0: o que queremos fazer?
Barras de rolagem. É isso que vamos criar. Mas você já pensou sobre o que eles fazem? Eu com certeza não. As barras de rolagem são um indicador de quanto do conteúdo disponível está visível no momento e quanto progresso você fez como leitor. Se você rolar para baixo, a barra de rolagem vai rolar para indicar que você está progredindo em direção ao final. Se todo o conteúdo couber na viewport, a barra de rolagem geralmente será oculta. Se o conteúdo tiver o dobro da altura da janela de visualização, a barra de rolagem vai preencher metade da altura da janela de visualização. Conteúdo com o triplo da altura da janela de visualização dimensiona a barra de rolagem para ⅓ da janela de visualização etc. Você entendeu o padrão. Em vez de rolar, você também pode clicar e arrastar a barra de rolagem para navegar pelo site mais rapidamente. Essa é uma quantidade surpreendente de comportamento para um elemento discreto como esse. Vamos lutar uma batalha de cada vez.
Etapa 1: inverter a direção
Podemos fazer com que os elementos se movam mais lentamente do que a velocidade de rolagem com transformações 3D do CSS, conforme descrito no artigo sobre rolagem de paralaxe. Também é possível reverter a direção? Acontece que podemos, e essa é a maneira de criar uma barra de rolagem personalizada com o tamanho exato do frame. Para entender como isso funciona, primeiro precisamos abordar alguns conceitos básicos do CSS 3D.
Para conseguir qualquer tipo de projeção em perspectiva no sentido matemático, é mais provável que você use coordenadas homogêneas. Não vou entrar em detalhes sobre o que elas são e por que funcionam, mas você pode pensar nelas como coordenadas 3D com uma quarta coordenada adicional chamada w. Essa coordenada deve ser 1, a menos que você queira ter distorção de perspectiva. Não precisamos nos preocupar com os detalhes de w, porque não vamos usar nenhum outro valor além de 1. Portanto, todos os pontos são vetores de 4 dimensões [x, y, z, w=1] e, consequentemente, as matrizes também precisam ser 4x4.
Uma ocasião em que você pode notar que o CSS usa coordenadas homogêneas
é quando você define suas próprias matrizes 4x4 em uma propriedade de transformação usando a
função matrix3d()
. matrix3d
usa 16 argumentos (porque a matriz é
4x4), especificando uma coluna após a outra. Podemos usar essa função para
especificar manualmente rotações, translações etc., mas ela também permite
mexer com a coordenada w.
Antes de usar matrix3d()
, precisamos de um contexto 3D, porque sem um
contexto 3D não há distorção de perspectiva e não é necessário usar
coordenadas homogêneas. Para criar um contexto 3D, precisamos de um contêiner com um
perspective
e alguns elementos que possam ser transformados no espaço 3D recém-criado. Por
exemplo:
Os elementos dentro de um contêiner de perspectiva são processados pelo mecanismo do CSS da seguinte maneira:
- Transforma cada canto (vértice) de um elemento em coordenadas homogêneas
[x,y,z,w]
, em relação ao contêiner de perspectiva. - Aplique todas as transformações do elemento como matrizes da direita para a esquerda.
- Se o elemento de perspectiva for rolável, aplique uma matriz de rolagem.
- Aplique a matriz de perspectiva.
A matriz de rolagem é uma tradução ao longo do eixo Y. Se rolarmos para baixo 400 px, todos os elementos precisam ser movidos para cima 400 px. A matriz de perspectiva é uma matriz que "puxa" os pontos para mais perto do ponto de fuga, quanto mais para trás eles estão no espaço 3D. Isso faz com que as coisas pareçam menores quando estão mais distantes e também as faz "se moverem mais devagar" quando são traduzidas. Portanto, se um elemento for empurrado para trás, uma tradução de 400 px fará com que o elemento se mova apenas 300 px na tela.
Se você quiser saber todos os detalhes, leia a especificação no modelo de renderização de transformação do CSS. No entanto, para este artigo, simplifiquei o algoritmo acima.
Nossa caixa está dentro de um contêiner de perspectiva com o valor p para o atributo
perspective
. Vamos supor que o contêiner pode ser rolado e que ele é rolado para baixo em
n pixels.
A primeira matriz é a matriz de perspectiva, a segunda é a matriz de rolagem. Para recapitular: o trabalho da matriz de rolagem é fazer com que um elemento se mova para cima quando estamos rolando para baixo, daí o sinal negativo.
No entanto, para a barra de rolagem, queremos o contrário: queremos que o elemento se mova para baixo quando rolarmos para baixo. Aqui é onde podemos usar um truque:
invertendo a coordenada w dos cantos da caixa. Se a coordenada w for
-1, todas as traduções vão entrar em vigor na direção oposta. Como fazer isso? O mecanismo do CSS converte os cantos da caixa em
coordenadas homogêneas e define w como 1. É hora de matrix3d()
brilhar!
.box {
transform:
matrix3d(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, -1
);
}
Essa matriz não vai fazer nada além de negar w. Portanto, quando o mecanismo do CSS
transforma cada canto em um vetor do formulário [x,y,z,1]
, a matriz o
converte em [x,y,z,-1]
.
Listei uma etapa intermediária para mostrar o efeito da matriz de transformação de elementos. Se você não se sente confortável com a matemática de matriz, tudo bem. O momento Eureka é que, na última linha, acabamos adicionando o deslocamento de rolagem n à nossa coordenada y em vez de subtrair. O elemento será movido para baixo se rolarmos para baixo.
No entanto, se colocarmos essa matriz no exemplo, o elemento não será exibido. Isso ocorre porque a especificação do CSS exige que qualquer vértice com w < 0 bloqueie a renderização do elemento. Como nossa coordenada z é atualmente 0 e p é 1, w será -1.
Felizmente, podemos escolher o valor de z. Para garantir que o resultado seja w=1, precisamos definir z = -2.
.box {
transform:
matrix3d(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, -1
)
translateZ(-2px);
}
Nossa caixa voltou!
Etapa 2: fazer com que ele se mova
Agora nossa caixa está lá e tem a mesma aparência que teria sem transformações. No momento, o contêiner de perspectiva não pode ser rolado, então não podemos vê-lo, mas sabemos que nosso elemento vai rolar na outra direção quando rolarmos. Vamos fazer o contêiner rolar, certo? Podemos adicionar um elemento de espaço que ocupa espaço:
<div class="container">
<div class="box"></div>
<span class="spacer"></span>
</div>
<style>
/* … all the styles from the previous example … */
.container {
overflow: scroll;
}
.spacer {
display: block;
height: 500px;
}
</style>
Agora role a caixa. A caixa vermelha se move para baixo.
Etapa 3: defina o tamanho
Temos um elemento que se move para baixo quando a página rola para baixo. Agora que a parte difícil acabou. Agora precisamos estilizar para que pareça uma barra de rolagem e tornar um pouco mais interativa.
Uma barra de rolagem geralmente consiste em um "círculo" e uma "faixa", e a faixa nem sempre está visível. A altura do polegar é diretamente proporcional à quantidade de conteúdo visível.
<script>
const scroller = document.querySelector('.container');
const thumb = document.querySelector('.box');
const scrollerHeight = scroller.getBoundingClientRect().height;
thumb.style.height = /* ??? */;
</script>
scrollerHeight
é a altura do elemento rolável, enquanto
scroller.scrollHeight
é a altura total do conteúdo rolável.
scrollerHeight/scroller.scrollHeight
é a fração do conteúdo que está
visível. A proporção do espaço vertical que o polegar cobre precisa ser igual à
proporção do conteúdo que está visível:
<script>
// …
thumb.style.height =
scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
// Accommodate for native scrollbars
thumb.style.right =
(scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>
O tamanho do polegar está bom, mas está se movendo muito rápido. É aqui que podemos usar nossa técnica do controle de paralaxe. Se movermos o elemento para trás, ele vai se mover mais devagar durante a rolagem. Podemos corrigir o tamanho aumentando-o. Mas quanto devemos recuar exatamente? Vamos fazer algumas contas. Prometo que é a última vez.
A informação crucial é que queremos que a borda inferior do polegar esteja
alinhada à borda inferior do elemento rolável quando rolado até o fim
para baixo. Em outras palavras, se rolarmos
scroller.scrollHeight - scroller.height
pixels, queremos que o polegar seja
movido por scroller.height - thumb.height
. Para cada pixel do controle deslizante, queremos que o polegar se mova uma fração de um pixel:
Esse é o nosso fator de escalonamento. Agora precisamos converter o fator de escalonamento em uma
translação ao longo do eixo z, o que já fizemos no artigo sobre rolagem
de paralaxe. De acordo com a
seção relevante da especificação:
o fator de escalonamento é igual a p/(p − z). Podemos resolver essa equação para z para
descobrir quanto precisamos traduzir nosso polegar ao longo do eixo z. Mas lembre-se
que, devido às nossas artimanhas com as coordenadas w, precisamos traduzir mais uma
-2px
ao longo de z. Observe também que as transformações de um elemento são aplicadas
da direita para a esquerda, o que significa que todas as traduções antes da matriz especial não serão
invertidas. Todas as traduções depois da matriz especial, no entanto, serão. Vamos
codificar isso.
<script>
// ... code from above...
const factor =
(scrollerHeight - thumbHeight)/(scroller.scrollHeight - scrollerHeight);
thumb.style.transform = `
matrix3d(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, -1
)
scale(${1/factor})
translateZ(${1 - 1/factor}px)
translateZ(-2px)
`;
</script>
Temos uma barra de rolagem. E é apenas um elemento DOM que podemos estilizar como quisermos. Uma coisa que é importante fazer em termos de acessibilidade é fazer com que o polegar responda ao clique e arraste, já que muitos usuários estão acostumados a interagir com uma barra de rolagem dessa maneira. Para não deixar esta postagem do blog ainda mais longa, não vou explicar os detalhes dessa parte. Confira o código da biblioteca para saber como isso é feito.
E o iOS?
Ah, meu velho amigo Safari para iOS. Assim como na rolagem com paralaxe, encontramos um
problema aqui. Como estamos rolando em um elemento, precisamos especificar
-webkit-overflow-scrolling: touch
, mas isso causa o achatamento 3D e todo o
efeito de rolagem para de funcionar. Resolvemos esse problema no scroller de paralaxe
detectando o Safari para iOS e usando position: sticky
como solução alternativa.
Vamos fazer exatamente a mesma coisa aqui. Consulte o
artigo sobre paralaxe
para refrescar sua memória.
E a barra de rolagem do navegador?
Em alguns sistemas, vamos ter que lidar com uma barra de rolagem nativa permanente.
Historicamente, a barra de rolagem não pode ser oculta (exceto com um
pseudoseletor não padrão).
Para ocultá-lo, precisamos recorrer a algumas técnicas de hacker (sem matemática). Envolvemos nosso
elemento de rolagem em um contêiner com overflow-x: hidden
e ampliamos o
elemento de rolagem para que ele fique mais largo que o contêiner. A barra de rolagem nativa do navegador agora está
fora da tela.
Nadadeira
Juntando tudo, agora podemos criar uma barra de rolagem personalizada perfeita, como a da demonstração do Nyan Cat.
Se você não conseguir ver o Nyan Cat, você está enfrentando um bug que encontramos e registramos ao criar essa demonstração (clique no polegar para fazer o Nyan Cat aparecer). O Chrome é muito bom em evitar trabalhos desnecessários, como pintar ou animar elementos fora da tela. A má notícia é que nossas manobras de matriz fazem o Chrome pensar que o GIF do Nyan Cat está fora da tela. Esperamos que isso seja corrigido em breve.
Pronto. Isso exigiu muito trabalho. Parabéns por ler tudo. Isso é uma pegadinha para fazer isso funcionar, e provavelmente vale a pena o esforço, exceto quando uma barra de rolagem personalizada é uma parte essencial da experiência. Mas é bom saber que isso é possível, não é? O fato de ser tão difícil fazer uma barra de rolagem personalizada mostra que há trabalho a ser feito no lado do CSS. Mas não se preocupe. No futuro, o AnimationWorklet do Houdini vai facilitar muito a criação de efeitos vinculados ao rolagem com frames perfeitos.