Não se preocupe, o paralaxe veio para ficar. Quando usada criteriosamente, ela adiciona profundidade e sutileza a um app da Web. No entanto, o problema é que implementar o paralaxe de maneira eficiente pode ser um desafio. Neste artigo, discutiremos uma solução que tem bom desempenho e funciona em vários navegadores.
Texto longo, leia o resumo
- Não use eventos de rolagem ou
background-position
para criar animações de paralaxe. - Use as transformações CSS 3D para criar um efeito de paralaxe mais preciso.
- No Mobile Safari, use
position: sticky
para garantir que o efeito paralaxe seja propagado.
Se você quiser a solução drop-in, acesse o repositório do GitHub com exemplos de elementos da interface e salve o JS auxiliar paralaxe. Confira uma demonstração ao vivo do botão de rolagem de paralaxe no repositório do GitHub.
Paralaxantes que apresentam problemas
Para começar, vamos analisar duas maneiras comuns de conseguir um efeito de paralaxe e, em particular, por que elas são inadequadas para nossos objetivos.
Ruim: uso de eventos de rolagem
O principal requisito do paralaxe é que ele precisa ser acoplado à rolagem. Para cada mudança na posição de rolagem da página, a posição do elemento de paralaxe precisa ser atualizada. Embora pareça simples, um mecanismo importante dos navegadores modernos é a capacidade de funcionar de forma assíncrona. Isso se aplica, no nosso caso específico, a eventos de rolagem. Na maioria dos navegadores, os eventos de rolagem são exibidos como "melhor esforço", e não há garantia de que eles serão exibidos em todos os frames da animação de rolagem.
Essa informação importante explica por que precisamos evitar uma solução baseada em JavaScript que mova elementos com base em eventos de rolagem: O JavaScript não garante que o paralaxe vai acompanhar a posição de rolagem da página. Nas versões mais antigas do Mobile Safari, os eventos de rolagem eram enviados ao final da rolagem, o que tornava impossível criar um efeito de rolagem baseado em JavaScript. As versões mais recentes fornecem eventos de rolagem durante a animação, mas, assim como o Chrome, com base no "melhor esforço". Se a linha de execução principal estiver ocupada com qualquer outro trabalho, os eventos de rolagem não serão entregues imediatamente, o que significa que o efeito paralaxe será perdido.
Ruim: atualizando background-position
Outra situação que gostaríamos de evitar é pintar em todos os frames. Muitas soluções
tentam mudar background-position
para fornecer a aparência de paralaxe, o que
faz com que o navegador repinte as partes afetadas da página ao rolar a tela, e isso
pode ser caro o suficiente para atrapalhar significativamente a animação.
Para cumprir a promessa do movimento de paralaxe, queremos algo que possa ser aplicado como uma propriedade acelerada (o que hoje significa manter as transformações e a opacidade) e que não dependa de eventos de rolagem.
CSS em 3D
Tanto Scott Kellum quanto Keith Clark fizeram um trabalho significativo na área de uso do CSS 3D para alcançar o movimento de paralaxe, e a técnica que eles usam é efetivamente esta:
- Configure um elemento contêiner para rolar a tela com
overflow-y: scroll
(e provavelmenteoverflow-x: hidden
). - Para esse mesmo elemento, aplique um valor
perspective
e umperspective-origin
definido comotop left
ou0 0
. - Aplique uma translação em Z aos filhos desse elemento e os dimensione de volta para oferecer movimento de paralaxe sem afetar o tamanho deles na tela.
O CSS para essa abordagem tem a seguinte aparência:
.container {
width: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: scroll;
perspective: 1px;
perspective-origin: 0 0;
}
.parallax-child {
transform-origin: 0 0;
transform: translateZ(-2px) scale(3);
}
Ele pressupõe um snippet de HTML como este:
<div class="container">
<div class="parallax-child"></div>
</div>
Ajustando a escala da perspectiva
Enviar o elemento filho de volta fará com que ele fique menor, proporcionalmente ao valor da perspectiva. É possível calcular o quanto ele precisará ser escalonado com esta equação: (perspectiva - distância) / perspectiva. Como provavelmente desejamos que o elemento de paralaxe seja paralaxe, mas aparece com o tamanho que o criou, ele precisaria ser dimensionado dessa forma, em vez de ficar como está.
No caso do código acima, a perspectiva é 1px, e a
distância Z de parallax-child
é -2px. Isso significa que o elemento precisará
ser escalonado verticalmente em 3x, que é o valor conectado ao código:
scale(3)
.
Para qualquer conteúdo sem um valor translateZ
aplicado, é possível
substituir um valor de zero. Isso significa que a escala é (perspective - 0) /
perspective, o que resulta em um valor de 1, o que significa que ela não foi escalonada
para cima nem para baixo. Isso é muito útil.
Como essa abordagem funciona
É importante esclarecer por que isso funciona, já que esse conhecimento será usado em breve. A rolagem é efetivamente uma transformação e, por isso, pode ser
acelerada. Ela envolve principalmente a troca de camadas com a GPU. Em uma
rolagem comum, ou seja, sem nenhuma noção de perspectiva, a rolagem
acontece de maneira individual ao comparar o elemento de rolagem e os filhos dele.
Se você rolar um elemento para baixo até 300px
, os filhos dele serão transformados
pelo mesmo valor: 300px
.
No entanto, aplicar um valor de perspectiva ao elemento de rolagem interfere
com esse processo. Isso muda as matrizes que sustentam a transformação de rolagem.
Agora, uma rolagem de 300 px só pode mover os filhos em 150 px, dependendo dos
valores de perspective
e translateZ
escolhidos. Se um elemento tiver um
valor translateZ
de 0, ele será rolado na proporção 1:1 (como costumava ser), mas um filho
empurrado em Z para longe da origem da perspectiva será rolado com uma taxa
diferente. Resultado: movimento de paralaxe. E, mais importante, isso é tratado automaticamente como parte do mecanismo de rolagem interno do navegador, o que significa que não é necessário ouvir eventos scroll
ou alterar background-position
.
Uma mosca na pomada: Mobile Safari
Há ressalvas para cada efeito, e uma importante para as transformações é a preservação de efeitos 3D em elementos filhos. Se houver elementos na hierarquia entre o elemento com uma perspectiva e os filhos de paralaxe, a perspectiva 3D será "nivelada", ou seja, o efeito será perdido.
<div class="container">
<div class="parallax-container">
<div class="parallax-child"></div>
</div>
</div>
No HTML acima, .parallax-container
é novo e nivela efetivamente o valor perspective
, e perdemos o efeito paralaxe. Na maioria dos casos,
a solução é bastante direta: você adiciona transform-style: preserve-3d
ao elemento, fazendo com que ele propague todos os efeitos 3D (como o valor da
perspectiva) que foram aplicados mais acima na árvore.
.parallax-container {
transform-style: preserve-3d;
}
No caso do Mobile Safari, no entanto, as coisas são um pouco mais complicadas.
A aplicação de overflow-y: scroll
ao elemento de contêiner funciona tecnicamente, mas
ao custo de deslizar o elemento de rolagem. A solução é adicionar
-webkit-overflow-scrolling: touch
, mas isso também nivelará o perspective
e não haverá paralaxe.
Do ponto de vista do aprimoramento progressivo, isso provavelmente não é um problema muito. Se não pudermos usar o efeito paralaxe em todas as situações, nosso app ainda vai funcionar, mas seria bom descobrir uma solução alternativa.
position: sticky
ao resgate!
Na verdade, há uma ajuda na forma de position: sticky
, que existe para
permitir que os elementos "fiquem" na parte de cima da janela de visualização ou de um determinado elemento pai
durante a rolagem. A especificação, como a maioria delas, é bastante pesada, mas contém uma
pequena joia útil:
Isso pode não parecer muito à primeira vista, mas um ponto importante dessa sentença é quando ela se refere exatamente a como a adesão de um elemento é calculada: "o deslocamento é calculado com referência ao ancestral mais próximo com uma caixa de rolagem". Em outras palavras, a distância para mover o elemento fixo (para que ele apareça anexado a outro elemento ou à janela de visualização) é calculada antes de qualquer outra transformação ser aplicada, não depois. Isso significa que, assim como no exemplo de rolagem anterior, se o deslocamento foi calculado em 300 px, há uma nova oportunidade de usar perspectivas (ou qualquer outra transformação) para manipular o valor de deslocamento de 300 px antes de ser aplicado a qualquer elemento fixo.
Ao aplicar position: -webkit-sticky
ao elemento de paralaxe, podemos "reverter" o efeito de achatamento de -webkit-overflow-scrolling:
touch
. Isso garante que o elemento de paralaxe faça referência ao ancestral mais próximo
com uma caixa de rolagem, que nesse caso é .container
. Em seguida,
de forma semelhante a antes, a .parallax-container
aplica um valor perspective
,
que muda o deslocamento de rolagem calculado e cria um efeito paralaxe.
<div class="container">
<div class="parallax-container">
<div class="parallax-child"></div>
</div>
</div>
.container {
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
}
.parallax-container {
perspective: 1px;
}
.parallax-child {
position: -webkit-sticky;
top: 0px;
transform: translate(-2px) scale(3);
}
Isso restaura o efeito paralaxe do Mobile Safari, o que é uma excelente notícia.
Ressalvas de posicionamento fixas
No entanto, há uma diferença aqui: position: sticky
altera a mecânica da paralaxe. O posicionamento fixo tenta fixar o elemento no
contêiner de rolagem, enquanto uma versão não fixa não. Isso significa que a
paralaxe com adesivo acaba sendo o inverso da sem:
- Com
position: sticky
, quanto mais próximo o elemento estiver de z=0, menos ele se moverá. - Sem
position: sticky
, quanto mais próximo o elemento estiver de z=0, mais ele se moverá.
Se tudo isso parece um pouco abstrato, consulte esta demonstração de Robert Flack, que mostra como os elementos se comportam de maneira diferente com e sem posicionamento fixo. Para ver a diferença, você precisa do Chrome Canary (que é a versão 56 na época em que este artigo foi escrito) ou do Safari.
Uma demonstração de Robert Flack (em inglês) mostrando como
position: sticky
afeta a rolagem de paralaxe.
Vários bugs e soluções alternativas
No entanto, como acontece com qualquer coisa, ainda há protuberâncias e protuberâncias que precisam ser suavizadas:
- O suporte fixo é inconsistente. O suporte ainda está sendo implementado no
Chrome, o Edge não oferece suporte total e o Firefox tem pintura de bugs quando aderente é combinado com transformações de perspectiva (link em inglês). Nesses
casos, vale a pena adicionar um pequeno código para adicionar
position: sticky
(a versão com o prefixo-webkit-
) apenas quando necessário, que é somente para o Mobile Safari. - O efeito não "simplesmente funciona" no Edge. O Edge tenta processar a rolagem no nível do SO, o que geralmente é bom, mas nesse caso ele impede a detecção das mudanças de perspectiva durante a rolagem. Para corrigir isso, adicione um elemento de posição fixa, já que isso parece trocar o Edge para um método de rolagem que não é do SO e garante que ele considere as mudanças de perspectiva.
- "O conteúdo da página ficou enorme!" Muitos navegadores consideram a escala
ao decidir o tamanho do conteúdo da página, mas, infelizmente, o Chrome e o Safari
não consideram a perspectiva. Portanto,
se houver uma escala de 3x aplicada a um elemento, você
vai ver barras de rolagem e similares, mesmo que o elemento esteja em 1x depois que a
perspective
tiver sido aplicada. É possível contornar esse problema dimensionando elementos no canto inferior direito (comtransform-origin: bottom right
), o que funciona porque faz com que elementos muito grandes cresçam na "região negativa" (geralmente o canto superior esquerdo) da área rolável. As regiões roláveis nunca permitem que você veja ou role até o conteúdo na região negativa.
Conclusão
O paralaxe é um efeito divertido quando usado com cuidado. Como você pode ver, é possível implementá-lo de uma forma que seja eficiente, com acoplamento de rolagem e entre navegadores. Como é necessário um pouco de distorção matemática e uma pequena quantidade de boilerplate para conseguir o efeito desejado, reunimos uma pequena biblioteca auxiliar e exemplo, que podem ser encontrados no nosso repositório de exemplos de elementos da interface no GitHub (link em inglês).
Curta o jogo e conte para nós como foi o processo.