O que Animação de Scroll estilo Apple faz?
Os efeitos são esfregados pela posição do scroll, não acionados por um temporizador. Rolar para cima reproduz o efeito ao contrário. Nenhuma biblioteca necessária.
As três ideias
- Pino. Um wrapper alto (ex.:
height: 300vh) contém um filho composition: sticky; top: 0; height: 100vh; overflow: hidden. O filho permanece na tela enquanto o wrapper passa rolando. A altura extra do wrapper é seu orçamento de animação. - Progresso. Uma função transforma a posição do scroll em um número de 0 a 1:
- seção fixa:
clamp(-rect.top / (rect.height - innerHeight)) - elemento entrando na viewport:
clamp((innerHeight*a - rect.top) / (innerHeight*(a-b)))
- seção fixa:
- Conduza os visuais a partir do número. Defina
translate,rotate,scale,opacity(eborderRadius) a cada quadro. Um único listener descrollregulado comrequestAnimationFramerecalcula tudo.
Motor pronto para colar: references/scroll-engine.js.
Efeitos que funcionam bem (escolha de 3 a 5, não todos)
| Efeito | Como fazer |
|---|---|
| Montar | Itens começam espalhados pela borda da viewport (ângulo aleatório, rotação, escala .35, opacidade .2) e suavizam até sua posição de layout. Calcule o deslocamento de cada item para seu local natural uma vez (ao carregar/redimensionar) e multiplique por (1 - ease(p)). Distribua por índice (stagger). |
| Iluminar palavras | Enrole cada palavra em um span. Opacidade .18 → 1 conforme clamp(p * 1.25 * N - k). |
| Stepper fixo | step = floor(p * N); a fração p*N - step preenche uma barra de progresso dentro do passo ativo. Clicar em um passo deve fazer scrollTo para a posição daquele passo. |
| Inclinar para plano | perspective(1400px) rotateX((1-e)*22deg) translateY((1-e)*60px) scale(.84 + .16*e) para um mockup de telefone/dispositivo. |
| Cartões que surgem | translate: 0 (1-e)*80px; scale: .9+.1e; opacity: e, com atraso por coluna (i % cols). |
| Expandir | Vídeo/imagem scale .78 → 1 e borderRadius 44px → 28px. |
Use ease = t => 1 - (1-t)**3 no progresso antes de aplicá-lo.
Regras (aprendidas da maneira difícil)
- Use as propriedades CSS individuais
translate,rotate,scalepara esfregar, e mantenhatransformpara hover. Caso contrário, hover e scroll disputam a mesma propriedade. - Não combine uma classe
.revealde fade com uma transformação esfregada no mesmo elemento. Escolha um. - Coloque o título da seção dentro do painel fixo com o conteúdo. Se o painel contiver apenas o conteúdo e estiver centralizado, monitores altos mostram uma grande área em branco.
- Mantenha a distância de scroll modesta. Cerca de
N * 55vh + 100vhpara um stepper de N etapas,240-300vhpara um efeito único. Usuários acham 700vh+ cansativo. - Limite o que é fixo ao que cabe em 100vh. Em telas curtas (
max-height: 820px) e celulares, oculte subtítulos e extras, e mostre apenas o passo ativo em um stepper. - Movimento reduzido: se
prefers-reduced-motion: reduce, retorne progresso1em todos os lugares (estado final, sem movimento). Não deixe coisas ocultas. - Sticky não precisa de
overflowem ancestrais excetooverflow-x: hiddennobody, que é aceitável. Eviteoverflow: hiddenem qualquer wrapper entre o elemento sticky e a página. - Recompute no redimensionamento (os deslocamentos de montagem dependem do layout).
- Anime apenas
translate/rotate/scale/opacity/border-radius; evite propriedades de layout. - Evite um segmento de vídeo inicial/final muito pequeno: ao esfregar a reprodução de
<video>, clipes curtos podem renderizar em branco.
Ordem típica da página
Hero (fixo, montar) → uma declaração simples (sem efeito) → prova/filme (expandir) → stepper fixo → mockup de dispositivo (inclinar para plano) → grade de cartões (surgir) → CTA. Alterne seções pretas e cinza claro para o estilo Apple; uma cor de destaque.
Testes
O --screenshot do Chrome headless não consegue rolar. Em vez disso, controle um Chrome real pelo protocolo DevTools:
chrome --headless=new --remote-debugging-port=9333 --user-data-dir=/tmp/prof about:blank &- Do Node (v22+ tem
WebSocketglobal): busquehttp://localhost:9333/json, abra owebSocketDebuggerUrlda página, envieEmulation.setDeviceMetricsOverride,Page.navigate, então para cada posiçãoRuntime.evaluate("scrollTo(0,Y)"), aguarde ~700 ms,Page.captureScreenshot. - Escolha Y a partir dos
offsetTope alturas das seções (ex.: 0%, 60%, 100% de cada seção fixa). - Verifique no desktop (1440x900), em uma tela grande (2000x1300) e em um celular (390x800), e confirme que
document.documentElement.scrollWidth === innerWidth(sem overflow horizontal).
Checklist de entrega
- Todo efeito conduzido pelo progresso do scroll, reversível
- Fallback para movimento reduzido
- Layout de celular verificado, sem scroll horizontal
- Comprimento do scroll fixo modesto; sem espaço em branco em telas altas
- Capturas de tela no início / meio / fim de cada seção fixa