Goste ou não, o efeito de paralaxe veio para ficar. Quando usado com moderação, ele pode adicionar profundidade e sutileza a um web app. No entanto, implementar o efeito de paralaxe de maneira eficiente pode ser um desafio. Neste artigo, vamos discutir uma solução eficiente e, igualmente importante, que funciona em vários navegadores.

TL;DR
- 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 em dispositivos móveis, use
position: sticky
para garantir que o efeito de paralaxe seja propagado.
Se quiser a solução pronta para uso, acesse o repositório do GitHub de amostras de elementos da interface e pegue o JS auxiliar de efeito de paralaxe. Confira uma demonstração ao vivo do rolador de paralaxe no repositório do GitHub.
Paralaxers de problemas
Para começar, vamos analisar duas maneiras comuns de conseguir 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 requisito principal do efeito paralaxe é que ele seja acoplado à rolagem. Para cada mudança na posição de rolagem da página, a posição do elemento de efeito paralaxe precisa ser atualizada. Embora pareça simples, um mecanismo importante dos navegadores modernos é a capacidade de trabalhar de forma assíncrona. No nosso caso específico, isso se aplica 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 serão entregues 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 o efeito de paralaxe acompanhe a posição de rolagem da página. Em versões mais antigas do Safari para dispositivos móveis, os eventos de rolagem eram entregues no final da rolagem, o que impossibilitava a criação de um efeito de rolagem baseado em JavaScript. Versões mais recentes fazem a entrega de eventos de rolagem durante a animação, mas, assim como o Chrome, com base no "melhor esforço". Se a thread principal estiver ocupada com qualquer outro trabalho, os eventos de rolagem não serão entregues imediatamente, o que significa que o efeito de paralaxe será perdido.
Incorreto: atualizando background-position
Outra situação que queremos evitar é a pintura em todos os frames. Muitas soluções tentam mudar background-position
para fornecer o efeito de paralaxe, o que faz com que o navegador pinte novamente as partes afetadas da página ao rolar. Isso pode ser caro o suficiente para prejudicar significativamente a animação.
Se quisermos cumprir a promessa de movimento de paralaxe, precisamos de algo que possa ser aplicado como uma propriedade acelerada (o que hoje significa usar transformações e 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 alcançar o movimento de paralaxe. A técnica usada por eles é esta:
- Configure um elemento de contêiner para rolar com
overflow-y: scroll
(e provavelmenteoverflow-x: hidden
). - Aplique a esse mesmo elemento um valor
perspective
e umperspective-origin
definido comotop left
ou0 0
. - Aplique uma translação em Z aos filhos desse elemento e redimensione-os para cima 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 pressupõe um snippet de HTML como este:
<div class="container">
<div class="parallax-child"></div>
</div>
Ajustar a escala para a perspectiva
Ao empurrar o elemento filho para trás, ele vai diminuir proporcionalmente ao valor da perspectiva. É possível calcular o quanto ele precisará ser ampliado com esta equação: (perspectiva - distância) / perspectiva. Como provavelmente queremos que o elemento de paralaxe tenha esse efeito, mas apareça no tamanho em que foi criado, ele precisa ser ampliado dessa forma, em vez de ser deixado como está.
No caso do código acima, a perspectiva é 1px, e a distância Z do
parallax-child
é -2px. Isso significa que o elemento precisará
ser ampliado 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 por um valor zero. Isso significa que a escala é (perspectiva - 0) / perspectiva, que resulta em um valor de 1, o que significa que ela não foi aumentada nem diminuída. É muito útil.
Como essa abordagem funciona
É importante deixar claro por que isso funciona, já que vamos usar esse conhecimento em breve. A rolagem é uma transformação, por isso pode ser acelerada. Ela envolve principalmente a movimentação de camadas com a GPU. Em uma rolagem típica, que não tem 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 para cima
pela 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, uma rolagem de 300 px pode mover os filhos em 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 era 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 do mecanismo de rolagem interno do navegador. Ou seja, não é necessário detectar eventos scroll
nem mudar background-position
.
Um problema: Safari para dispositivos móveis
Há ressalvas para cada efeito, e uma importante para transformações é sobre a preservação de efeitos 3D para elementos filhos. Se houver elementos na hierarquia entre o elemento com uma perspectiva e os filhos com efeito de 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 efetivamente
achatar o valor perspective
, e perdemos o efeito de paralaxe. A solução, na maioria dos casos, é bem simples: adicione transform-style: preserve-3d
ao elemento, fazendo com 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 entanto, no caso do Safari para dispositivos móveis, as coisas são um pouco mais complicadas.
Aplicar overflow-y: scroll
ao elemento contêiner funciona tecnicamente, mas ao custo de não poder mover o elemento de rolagem. A solução é adicionar -webkit-overflow-scrolling: touch
, mas isso também vai achatar o perspective
e não teremos efeito de paralaxe.
Do ponto de vista do aprimoramento progressivo, isso provavelmente não é um problema muito grande. Se não for possível usar o efeito de paralaxe em todas as situações, o app ainda vai funcionar, mas seria bom encontrar uma solução alternativa.
position: sticky
para ajudar!
Na verdade, há uma ajuda na forma de position: sticky
, que existe para permitir que os elementos "fiquem" na parte superior da janela de visualização ou em um determinado elemento pai durante a rolagem. A especificação, como a maioria delas, é bastante extensa, mas contém uma
pequena joia útil:
Isso pode não parecer muito à primeira vista, mas um ponto importante nessa frase é quando ela se refere a como, exatamente, a fixaçã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, e 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 que ele seja 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, nesse caso, é .container
. Assim como antes, 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!
Advertências sobre o posicionamento fixo
No entanto, há uma diferença: position: sticky
altera a mecânica de
paralaxe. O posicionamento fixo tenta, bem, fixar o elemento ao
contêiner de rolagem, enquanto uma versão não fixa não faz isso. Isso significa que o efeito de paralaxe com fixação acaba sendo o inverso do que não tem:
- Com
position: sticky
, quanto mais próximo o elemento estiver de z=0, menos ele se moverá. - Sem
position: sticky
, quanto mais perto o elemento estiver de z=0, mais ele se moverá.
Se tudo isso parece um pouco abstrato, confira esta demonstração (link em inglês) 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 no momento da redação) ou do Safari.

Uma demonstração de Robert Flack mostrando como
position: sticky
afeta a rolagem paralaxe.
Vários bugs e soluções alternativas
No entanto, como em qualquer coisa, ainda há problemas que precisam ser resolvidos:
- O suporte fixo é inconsistente. A compatibilidade ainda está sendo implementada no Chrome, o Edge não tem suporte e o Firefox tem bugs de renderização quando o sticky é combinado com transformações de perspectiva. Nesses casos, vale a pena adicionar um pouco de código para incluir apenas
position: sticky
(a versão com prefixo-webkit-
) quando necessário, ou seja, apenas para o Safari para dispositivos móveis. - 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, impede que ele detecte as mudanças de perspectiva durante a rolagem. Para corrigir isso, adicione um elemento de posição fixa, já que isso parece mudar o Edge para um método de rolagem não relacionado ao 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, por exemplo, uma escala de 3x aplicada a um elemento, talvez você veja barras de rolagem e semelhantes, mesmo que o elemento esteja em 1x depois que o
perspective
for aplicado. É possível contornar esse problema dimensionando elementos do canto inferior direito (comtransform-origin: bottom right
), o que funciona porque faz com que elementos grandes demais cresçam na "região negativa" (normalmente a parte superior esquerda) da área rolável. As regiões roláveis nunca permitem ver ou rolar até o conteúdo na região negativa.
Conclusão
O efeito de paralaxe é divertido quando usado com cuidado. Como você pode ver, é possível implementá-lo de uma forma eficiente, acoplada à rolagem e compatível com vários navegadores. Como isso exige um pouco de matemática e uma pequena quantidade de boilerplate para alcançar o efeito desejado, criamos uma pequena biblioteca auxiliar e um exemplo, que podem ser encontrados no repositório do GitHub de exemplos de elementos da interface.
Teste e conte para nós como foi.