Ame-o ou odeie-o, o efeito paralaxe veio para ficar. Quando usada com cuidado, ela pode adicionar profundidade e sutileza a um app da Web. No entanto, o problema é que a implementação da paralaxe de forma eficiente pode ser desafiadora. Neste artigo, vamos discutir uma solução que tenha um bom desempenho e, igualmente importante, que funcione em vários navegadores.
Texto longo, leia o resumo
- Não use eventos de rolagem ou
background-position
para criar animações de paralaxe. - Usar transformações CSS 3D para criar um efeito de paralaxe mais preciso
- No Safari para dispositivos móveis, use
position: sticky
para garantir que o efeito de paralaxe seja propagado.
Se você quiser a solução drop-in, acesse o repositório de exemplos de elementos da interface do GitHub no GitHub (em inglês) e faça o JS auxiliar do Parallax. Confira uma demonstração ao vivo do botão de rolagem paralaxe no repositório do GitHub.
Paralaxadores de problema
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: usar eventos de rolagem
O principal requisito do paralaxe é que ele precisa ser associado à 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 trabalhar 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 entregues como "melhor esforço" e não há garantia de que isso aconteça em todos os frames da animação de rolagem.
Essa informação importante nos diz 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 continuar acompanhando a posição de rolagem da página. Em versões mais antigas do Mobile Safari, os eventos de rolagem eram exibidos no final da rolagem, o que tornava impossível criar um efeito de rolagem baseado em JavaScript. Versões mais recentes oferecem eventos de rolagem durante a animação, mas, assim como o Chrome, de acordo com o "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 quadros. Muitas soluções
tentam mudar o background-position
para oferecer a aparência de paralaxe, o que
faz com que o navegador mostre novamente as partes afetadas da página na rolagem, e isso
pode ser caro o suficiente para causar uma instabilidade significativa na animação.
Se quisermos cumprir a promessa do movimento paralaxe, queremos algo que possa ser aplicado como uma propriedade acelerada (o que atualmente significa manter as transformações e a opacidade) e que não dependa de eventos de rolagem.
CSS em 3D
Scott Kellum e Keith Clark fizeram um trabalho significativo na área de uso do CSS 3D para gerar movimento de paralaxe, e a técnica usada é esta:
- Configure um elemento contêiner para rolar com
overflow-y: scroll
(e provavelmenteoverflow-x: hidden
). - Aplique um valor
perspective
a esse mesmo elemento e definaperspective-origin
comotop left
ou0 0
. - Aos filhos desse elemento, aplique uma translação em Z e redimensione-os para fornecer 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);
}
O que pressupõe um snippet de HTML como este:
<div class="container">
<div class="parallax-child"></div>
</div>
Ajustando escala para perspectiva
Reverter o elemento filho fará com que ele fique menor proporcional ao valor da perspectiva. É possível calcular quanto ele precisará ser ampliado com esta equação: (perspectiva - distância) / perspectiva. Como provavelmente queremos que o elemento de paralaxe paralaxe, mas apareça no tamanho em que você o criou, ele precisaria ser dimensionado dessa forma, em vez de ser deixado como está.
No caso do código acima, a perspectiva é de 1px e a
distância Z da parallax-child
é de -2px. Isso significa que o elemento precisará
ser dimensionado em 3x, que é o valor conectado ao código:
scale(3)
.
Para qualquer conteúdo que não tenha um valor de translateZ
aplicado, é possível
substituir um valor de zero. Isso significa que a escala é (perspective - 0) /
perspective, com um valor de 1, o que significa que o escalonamento
não foi feito para cima ou para baixo. Isso é bastante útil.
Como essa abordagem funciona
É importante deixar claro por que isso funciona, pois vamos usar
esse conhecimento em breve. A rolagem é efetivamente uma transformação, e é por isso que ela pode ser
acelerada. Ela envolve principalmente a mudança de camadas com a GPU. Em uma
rolagem típica, que não tem noção de perspectiva, a rolagem
acontece de forma individual ao comparar o elemento de rolagem e os filhos dele.
Se você rolar um elemento para baixo em 300px
, os filhos dele serão transformados
para cima no mesmo valor: 300px
.
No entanto, a aplicação de um valor de perspectiva ao elemento de rolagem afeta
esse processo. Isso muda as matrizes que sustentam a transformação de rolagem.
Agora, uma rolagem de 300 pixels só pode mover os filhos em 150 pixels, dependendo dos
valores de perspective
e translateZ
escolhidos. Se um elemento tiver um
valor translateZ
de 0, ele será rolado em 1:1 (como costumava ser), mas um filho
empurrado Z para fora da origem da perspectiva será rolado a uma taxa
diferente. Resultado: movimento de paralaxe. E, o mais importante, isso é processado automaticamente como
parte do mecanismo de rolagem interno do navegador, o que significa que não
é necessário detectar eventos scroll
ou mudar background-position
.
Uma mosca na pomba: Safari para dispositivos móveis
Há ressalvas para todos os efeitos, 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", o que significa que o efeito será perdido.
<div class="container">
<div class="parallax-container">
<div class="parallax-child"></div>
</div>
</div>
No HTML acima, a .parallax-container
é nova e nivela 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 quaisquer efeitos 3D (como o valor da
perspectiva) que tenham sido aplicados mais acima na árvore.
.parallax-container {
transform-style: preserve-3d;
}
No caso do Mobile Safari, porém, as coisas são um pouco mais complicadas.
A aplicação de overflow-y: scroll
ao elemento de contêiner funciona tecnicamente, mas
à custa da capacidade de lançar o elemento de rolagem. A solução é adicionar
-webkit-overflow-scrolling: touch
, mas isso também nivela a perspective
e não haverá paralaxe.
Do ponto de vista do aprimoramento progressivo, isso não é um problema muito grande. Se não for possível 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á alguma ajuda na forma de position: sticky
, que existe para
permitir que os elementos "se fixem" na parte de cima da janela de visualização ou em um determinado elemento pai
durante a rolagem. As especificações, como a maioria delas, são bem grandes, mas contêm uma
pequena joia útil dentro dela:
Isso pode não parecer muito à primeira vista, mas um ponto importante nessa frase é quando se refere a como exatamente a aderência 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 for calculado em 300 px, há uma nova oportunidade de usar perspectivas (ou qualquer outra transformação) para manipular esse valor de deslocamento de 300 pixels antes que ele seja aplicado a elementos fixos.
Ao aplicar position: -webkit-sticky
ao elemento de paralaxe, podemos "reverter" efetivamente o efeito de nivelamento 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,
da mesma forma que antes, .parallax-container
aplica um valor perspective
,
que muda o deslocamento de rolagem calculado e cria um efeito de 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 fixo
Há uma diferença aqui, no entanto: position: sticky
altera a
mecanismo de paralaxe. O posicionamento fixo tenta, bem, prender o elemento ao
contêiner de rolagem, enquanto uma versão não fixa não. Isso significa que o
paralaxe com fixa acaba sendo o inverso daquele sem:
- Com
position: sticky
, quanto mais próximo o elemento for de z=0, menos ele se moverá. - Sem
position: sticky
, quanto mais próximo o elemento for de z=0, mais ele se moverá.
Se tudo isso parece um pouco abstrato, confira 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 mostrando como
position: sticky
afeta a rolagem paralaxe.
Diversos bugs e soluções alternativas
No entanto, como acontece com tudo, ainda há caroços que precisam ser suavizados:
- O suporte fixo é inconsistente. O suporte ainda está sendo implementado no
Chrome, o Edge não é totalmente compatível, e o Firefox apresenta bugs quando o modo aderente é combinado com transformações de perspectiva. Nesses
casos, vale a pena adicionar um pequeno código para adicionar apenas
position: sticky
(a versão com prefixo-webkit-
) quando necessário, que é somente para o Mobile Safari. - O efeito não "apenas funciona" no Edge. O Edge tenta processar a rolagem no nível do SO, o que geralmente é bom, mas, nesse caso, impede que ele detecte as mudanças de perspectiva durante a rolagem. Para corrigir isso, adicione um elemento de posição fixa, já que ele parece mudar 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, é possível
ver barras de rolagem e similares, mesmo que o elemento esteja em 1x após a
aplicação de
perspective
. É possível contornar esse problema dimensionando elementos do 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 de rolagem. As regiões roláveis nunca permitem que você veja ou role para o conteúdo na região negativa.
Conclusão
A paralaxe é um efeito divertido quando usado de maneira cuidadosa. É possível implementá-lo de uma maneira eficiente, compatível com rolagem e entre navegadores. Como ela requer um pouco de confusão matemática e uma pequena quantidade de texto boilerplate para alcançar o efeito desejado, reunimos uma pequena biblioteca auxiliar e uma amostra, que podem ser encontradas no nosso repositório de amostras de elementos da interface no GitHub (link em inglês).
Jogue agora e nos conte como você se saiu.