Goste ou não, o efeito parallax veio para ficar. Quando usado com cuidado, ele pode adicionar profundidade e sutileza a um app da Web. O problema, no entanto, é que implementar a paralaxe de maneira eficiente pode ser desafiador. Neste artigo, vamos discutir uma solução que tem bom desempenho e, igualmente importante, 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 transformações 3D do CSS para criar um efeito de paralaxe mais preciso.
- Para o Safari para dispositivos móveis, use
position: sticky
para garantir que o efeito de paralaxe seja propagado.
Se você quiser a solução de inclusão, acesse o repositório de amostras de elementos da interface do GitHub e pegue o JS auxiliar de paralaxe. Confira uma demonstração ao vivo do scroller de paralaxe no repositório do GitHub.
Problemas de paralaxe
Para começar, vamos analisar duas maneiras comuns de criar um efeito de paralaxe e, em particular, por que elas não são adequadas para nossos propósitos.
Incorreto: usar eventos de rolagem
O principal requisito da paralaxe é que ela seja acoplada à rolagem. Para cada mudança na posição de rolagem da página, a posição do elemento de paralaxe precisa ser atualizada. Embora isso 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 enviados como "melhor esforço" e não há garantia de que serão enviados 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 move elementos com base em eventos de rolagem: o JavaScript não garante que a paralaxe vai acompanhar a posição de rolagem da página. Nas versões mais antigas do Safari para dispositivos móveis, os eventos de rolagem eram enviados no final da rolagem, o que tornava impossível criar um efeito de rolagem baseado em JavaScript. As versões mais recentes fazem a entrega de eventos de rolagem durante a animação, mas, assim como no Chrome, de forma "máxima". Se a linha de execução principal estiver ocupada com qualquer outro trabalho, os eventos de rolagem não serão enviados imediatamente, o que significa que o efeito de paralaxe será perdido.
Incorreto: atualização de 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 pinte novamente as partes afetadas da página ao rolar, o que
pode ser caro o suficiente para prejudicar significativamente a animação.
Se quisermos cumprir a promessa do movimento de paralaxe, precisamos de algo que possa ser aplicado como uma propriedade acelerada (o que hoje significa aderir a transformações e opacidade) e que não dependa de eventos de rolagem.
CSS em 3D
Scott Kellum e Keith Clark fizeram trabalhos significativos na área de uso do CSS 3D para alcançar o movimento de paralaxe, e a técnica que eles usam é basicamente esta:
- Configure um elemento de contêiner para rolar com
overflow-y: scroll
(e provavelmenteoverflow-x: hidden
). - Para esse mesmo elemento, aplique um valor
perspective
e umperspective-origin
definido comotop left
ou0 0
. - Para os filhos desse elemento, aplique uma tradução em Z e dimensione-os novamente para fornecer movimento de paralaxe sem afetar o tamanho deles na tela.
O CSS para essa abordagem é assim:
.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);
}
Que assume um snippet de HTML como este:
<div class="container">
<div class="parallax-child"></div>
</div>
Como ajustar a escala para a perspectiva
Empurrar o elemento filho para trás faz com que ele fique menor proporcionalmente ao valor da perspectiva. É possível calcular quanto será necessário aumentar o tamanho com esta equação: (perspectiva - distância) / perspectiva. Como provavelmente queremos que o elemento de paralaxe tenha paralaxe, mas apareça no tamanho que criamos, ele precisa ser aumentado dessa forma, em vez de ser deixado 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 dimensionado em 3x, que é o valor inserido no código:
scale(3)
.
Para qualquer conteúdo que não tenha um valor translateZ
aplicado, você pode
substituir um valor de zero. Isso significa que a escala é (perspectiva - 0) /
perspectiva, que resulta em um valor de 1, o que significa que ela não foi dimensionada
para cima ou para baixo. Muito útil, na verdade.
Como essa abordagem funciona
É importante entender por que isso funciona, já que vamos usar esse
conhecimento em breve. O rolagem é uma transformação, e por isso pode ser
acelerada. Ela envolve principalmente a mudança de camadas com a GPU. Em uma
rolagem típica, que é uma rolagem sem nenhuma noção de perspectiva, a rolagem
acontece de maneira 1:1 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
na mesma quantidade: 300px
.
No entanto, aplicar um valor de perspectiva ao elemento de rolagem atrapalha
esse processo, mudando as matrizes que sustentam a transformação de rolagem.
Agora, um rolagem de 300 px pode mover as crianças apenas 150 px, dependendo dos
valores de perspective
e translateZ
escolhidos. Se um elemento tiver um
valor translateZ
de 0, ele será rolado em 1:1 (como antes), mas um filho
empurrado em Z para longe da origem da perspectiva será rolado em uma taxa
diferente. Resultado líquido: movimento de paralaxe. E, muito importante, isso é processado automaticamente como
parte da maquinaria de rolagem interna do navegador, o que significa que não é
preciso detectar eventos scroll
ou mudar background-position
.
Uma mosca na sopa: Safari para dispositivos móveis
Há ressalvas para cada efeito, e uma importante para transformações é a preservação de efeitos 3D para elementos filhos. Se houver elementos na hierarquia entre o elemento com uma perspectiva e os filhos com paralaxe, a perspectiva 3D será "achatada", ou seja, o efeito será perdido.
<div class="container">
<div class="parallax-container">
<div class="parallax-child"></div>
</div>
</div>
No HTML acima, o .parallax-container
é novo e vai
achatar o valor perspective
, fazendo com que o efeito de paralaxe seja perdido. A solução,
na maioria dos casos, é bastante simples: adicione transform-style: preserve-3d
ao elemento para que ele propague todos os efeitos 3D (como nosso valor de
perspectiva) que foram aplicados mais acima na árvore.
.parallax-container {
transform-style: preserve-3d;
}
No caso do Safari para dispositivos móveis, as coisas são um pouco mais complicadas.
Aplicar overflow-y: scroll
ao elemento do contêiner funciona tecnicamente, mas
com o custo de poder deslizar o elemento de rolagem. A solução é adicionar
-webkit-overflow-scrolling: touch
, mas isso também vai nivelar o perspective
e não vamos ter nenhum efeito de paralaxe.
Do ponto de vista de melhoria progressiva, isso provavelmente não é um problema muito grande. Se não for possível usar a paralaxe em todas as situações, o app ainda vai funcionar, mas seria bom encontrar 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 viewport 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 importante à primeira vista, mas um ponto-chave dessa frase é quando ela 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 à viewport) é 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 esse valor de deslocamento de 300 px antes de ser aplicado a elementos fixos.
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, neste caso, é .container
. Em seguida,
semelhante ao anterior, o .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 de paralaxe para o Safari para dispositivos móveis, o que é uma ótima notícia para todos.
Advertências sobre posicionamento fixo
No entanto, há uma diferença aqui: o position: sticky
altera a
mecânica de paralaxe. A posição fixa tenta fixar o elemento no
contêiner de rolagem, enquanto uma versão não fixa não faz isso. Isso significa que a
paralaxe com fixação acaba sendo o inverso da outra sem:
- Com
position: sticky
, quanto mais próximo do z=0 o elemento estiver, menos ele se move. - Sem
position: sticky
, quanto mais próximo do z=0 o elemento estiver, mais ele se move.
Se isso parecer um pouco abstrato, confira esta demonstração de Robert Flack, que demonstra como os elementos se comportam de maneira diferente com e sem o posicionamento fixo. Para notar a diferença, você precisa do Chrome Canary (versão 56 no momento da escrita) ou do Safari.
Uma demonstração de Robert Flack mostrando como
position: sticky
afeta a rolagem de paralaxe.
Vários bugs e soluções alternativas
No entanto, como em qualquer coisa, ainda há problemas que precisam ser resolvidos:
- O suporte a "sticky" é inconsistente. O suporte ainda está sendo implementado no
Chrome, o Edge não tem suporte e o Firefox tem bugs de pintura quando a aderência é combinada com transformações de perspectiva. Nesses
casos, vale a pena adicionar um pouco de código para adicionar apenas
position: sticky
(a versão com prefixo-webkit-
) quando necessário, que é apenas para o Safari para dispositivos móveis. - O efeito não "apenas funciona" no Edge. O Edge tenta processar a rolagem no nível do SO, o que geralmente é uma coisa boa, 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 ele parece mudar o Edge para um método de rolagem que não é do SO e garante que ele leve em conta 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ê poderá
ver barras de rolagem e similares, mesmo que o elemento esteja em 1x depois que o
perspective
tiver sido aplicado. É possível contornar esse problema redimensionando elementos do canto inferior direito (comtransform-origin: bottom right
). Isso funciona porque faz com que elementos grandes cresçam na "região negativa" (normalmente no canto superior esquerdo) da área rolável. As regiões roláveis nunca permitem que você veja ou role o conteúdo na região negativa.
Conclusão
A paralaxe é um efeito divertido quando usado com cuidado. Como você pode ver, é possível implementá-lo de uma maneira que tenha bom desempenho, seja acoplado ao rolagem e funcione em vários navegadores. Como ele requer um pouco de manobra matemática e uma pequena quantidade de boilerplate para conseguir o efeito desejado, criamos uma pequena biblioteca auxiliar e uma amostra, que você pode encontrar no nosso repositório de amostras de elementos da interface do GitHub.
Teste e nos diga o que achou.