Ação disparada no scroll: o que é ajuda e o que é armadilha para quem lê
Como usar a posição da rolagem para carregar imagem, mostrar progresso e paginar sem abrir modal surpresa, travar o fundo, esconder o rodapé ou inflar a métrica de leitura.
O blog de receitas mede quanto cada visitante rolou. Ao passar de 40% da página, abre um modal da newsletter e o fundo trava. A pessoa estava com a mão suja de farinha, no meio do modo de preparo, e teve que caçar um "x" de 12 pixels. A métrica de cadastros subiu um pouco. O tempo na página e o retorno de visitantes caíram bem mais.
A resposta curta: rolar a página não é clique nem consentimento. Use a posição da rolagem para o que ajuda a leitura: carregar imagem perto da hora de aparecer, mostrar onde a pessoa está, buscar mais itens quando ela pedir. Não amarre a um percentual de rolagem nada que interrompa, como modal, trava de fundo, troca de página ou envio de dados. E se medir "chegou ao fim", chame de "chegou ao fim", não de "leu".
Por que o scroll virou gatilho
Descobrir a posição de um elemento é fácil: elemento.getBoundingClientRect().top diz quantos pixels ele está do topo da área visível. Com isso, qualquer coisa pode acontecer "quando a pessoa chegar aqui". A técnica é neutra. O problema é que ela permite agir sem pedir. Um clique em "Assinar" é uma decisão. Rolar até o meio de uma receita é só ler a receita.
O próprio Google tem orientação pública contra intersticiais intrusivos que cobrem o conteúdo no celular. Mesmo sem pensar em busca, a conta é simples: tudo o que interrompe a leitura cobra de quem veio ler.
O que pode depender do scroll
- Carregar imagem e vídeo abaixo da dobra: economiza dados e acelera a primeira tela.
- Indicar progresso e posição: barra fina de progresso, item do sumário destacado.
- Mostrar o botão "voltar ao topo" depois de uma rolagem longa.
- Pedir mais itens numa lista, desde que o rodapé continue alcançável.
O que não deve depender do scroll
- Abrir modal, pop-up de newsletter ou pedido de notificação.
- Travar o fundo com
overflow: hiddennobody. - Marcar algo como "lido", "aceito" ou "visto" com efeito para a pessoa.
- Esconder conteúdo até a pessoa rolar, de um jeito que some se o script falhar.
Como fazer cada uso legítimo
- Para imagens, use primeiro o recurso nativo:
loading="lazy"noimg. Não precisa de script. - Para saber se um bloco entrou na tela, use
IntersectionObserverem vez de ouvir cada evento de rolagem. O guia saber quando um bloco entrou na tela mostra a técnica em detalhe. - Se precisar mesmo do evento
scroll, registre com{ passive: true }e faça só leitura barata dentro dele. - Para listas longas, ofereça um botão "Carregar mais" que a pessoa aciona.
- Respeite
prefers-reduced-motionem toda animação ligada à rolagem.
A imagem do passo a passo da receita, sem uma linha de JavaScript:
<img src="/receitas/pao-de-queijo/passo-3.jpg"
alt="Massa sendo enrolada em bolinhas de 3 cm"
width="800" height="600"
loading="lazy">
Largura e altura no HTML reservam o espaço da imagem antes de ela chegar, e o texto não pula quando ela carrega. Não use loading="lazy" na imagem principal do topo: ela precisa vir logo.
Lista que carrega mais sem sequestrar o rodapé
Rolagem infinita automática transforma o rodapé num lugar inalcançável: cada vez que a pessoa chega perto do contato ou da política de privacidade, mais itens empurram tudo para baixo. Um botão resolve, e ainda permite voltar ao ponto certo:
<ol id="lista-receitas">...</ol>
<button type="button" id="carregar-mais" data-pagina="2">Carregar mais receitas</button>
<script>
const botao = document.getElementById('carregar-mais');
const lista = document.getElementById('lista-receitas');
botao.addEventListener('click', async function () {
botao.disabled = true;
botao.textContent = 'Carregando...';
try {
const pagina = botao.dataset.pagina;
const resposta = await fetch('/receitas?pagina=' + pagina + '&formato=itens');
if (!resposta.ok) throw new Error('HTTP ' + resposta.status);
const antes = lista.children.length;
lista.insertAdjacentHTML('beforeend', await resposta.text());
const primeiroNovo = lista.children[antes];
if (primeiroNovo) {
primeiroNovo.setAttribute('tabindex', '-1');
primeiroNovo.focus();
}
botao.dataset.pagina = String(Number(pagina) + 1);
botao.textContent = 'Carregar mais receitas';
} catch (erro) {
botao.textContent = 'Falhou. Tentar de novo';
} finally {
botao.disabled = false;
}
});
</script>
O foco vai para o primeiro item novo, então quem usa teclado ou leitor de tela continua de onde parou, em vez de voltar ao começo da lista. Para listas paginadas no servidor, combine com a técnica de paginação que não pula nem repete item.
Métrica de leitura honesta
"Rolou até 100%" não é "leu". A pessoa pode ter descido direto para os comentários. Se a equipe quer saber quantos chegaram ao fim do texto, meça exatamente isso, com um marcador no fim do artigo e um envio que não atrasa a navegação:
const fim = document.getElementById('fim-do-artigo');
const observador = new IntersectionObserver(function (entradas) {
if (entradas[0].isIntersecting) {
navigator.sendBeacon('/metricas', JSON.stringify({ evento: 'chegou_ao_fim', pagina: location.pathname }));
observador.disconnect();
}
});
observador.observe(fim);
O nome do evento é chegou_ao_fim, não leu_o_artigo. Parece detalhe, mas é o que impede o relatório de prometer algo que o dado não mostra. Se a métrica envolver identificar a pessoa, ela entra nas mesmas regras de consentimento do resto da análise de uso.
Animação de revelar no scroll
Muito tema "moderno" começa com os blocos em opacity: 0 e só os mostra quando entram na tela. Se o script falhar, carregar devagar ou for bloqueado, o conteúdo fica invisível. Faça o contrário: o conteúdo nasce visível e a animação é um enfeite opcional, desligado para quem pediu menos movimento:
@media (prefers-reduced-motion: no-preference) {
.revelar {
transition: opacity 0.4s, transform 0.4s;
}
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
Erros comuns
- Modal amarrado a um percentual: interrompe a leitura no pior momento e ensina a pessoa a sair do site.
- Travar o fundo e não destravar: se o modal falha ou é fechado por um caminho não previsto, a página fica sem rolagem.
- Rolagem infinita sem botão: o rodapé, com contato e política, nunca é alcançado.
- Conteúdo invisível até rolar: com o script bloqueado ou lento, a página aparece vazia.
- Chamar "rolou" de "leu": o relatório decide pauta com um número que não mede leitura.
- Ouvir
scrollcom trabalho pesado: a rolagem engasga no celular, justamente onde a pessoa mais rola.
Checklist
- Nenhum modal ou pedido de permissão abre só porque a pessoa rolou.
- O fundo nunca fica travado sem um modal aberto por decisão da pessoa.
- Imagens abaixo da dobra usam
loading="lazy"com largura e altura. - Listas longas têm "Carregar mais" e o rodapé é alcançável.
- O conteúdo aparece com JavaScript desligado.
- Animações de rolagem respeitam
prefers-reduced-motion. - A métrica tem o nome do que realmente mede.
Abra o seu site no celular e role de uma ponta à outra sem tocar em nada além da tela: você chega ao rodapé sem fechar nenhuma janela no caminho?
Referência: http://pt.stackoverflow.com/questions/41088/descobrir-posi%C3%A7%C3%A3o-do-scroll-de-um-determinado-objeto