voltar ao blog

Âncoras que não rolavam: o bug invisível que passou 15 dias no ar (e a feature que o denunciou)

Se um link de âncora do seu site muda a URL mas a página não rola até a seção, a causa provável é a combinação de scroll-behavior: smooth no html com overflow-x: hidden no bodyno Chromium, essa dupla mata a animação de scroll no primeiro frame, e todo pulo de âncora simplesmente não acontece. A correção cabe em duas linhas de CSS: trocar overflow-x: hidden por overflow-x: clip e repensar o scroll suave. Este post é a história real de como esse bug ficou 15 dias invisível no ar, neste site, e de como uma feature nova o denunciou.

Eu sou o Filipe, fundador da Logup Digital, desenvolvedor full-stack há mais de 12 anos. Já depurei de deadlock de banco a memory leak em produção, e a lição que esse tempo todo ensina é sempre a mesma: bug silencioso é o pior bug — ele não gera erro no console, não gera reclamação, só gera visitante desistindo. O caso de hoje é exatamente dessa família.

Vou te mostrar a cronologia da caçada, hipótese por hipótese descartada, a causa raiz, a correção — e a lição de QA que mudou meu checklist de entrega.

O sintoma: a URL muda, a página não

O contexto: eu tinha acabado de implementar o índice automático com âncoras que todo artigo deste blog ganhou. Feature no ar, HTML perfeito, âncoras geradas com o parser do core do WordPress. Fui testar o clique num item do índice e… nada. A barra de endereço ganhou o #nome-da-secao, mas o scroll ficou parado no mesmo lugar.

Primeiro reflexo de qualquer dev: “quebrei algo agora”. Só que o teste seguinte mudou o tamanho do problema: os links do menu principal do site — aqueles /#servicos, /#faq que existem desde o lançamento — também nunca tinham rolado a página. O site entrou no ar em 29 de julho; a descoberta foi em 13 de agosto. Quinze dias com a navegação por âncora morta no Chrome e no Edge, sem um único erro de console, sem uma única reclamação. O índice novo não criou o bug: ele o denunciou.

A caçada: descartando hipóteses uma por uma

Debugging sistemático é descartar causas em ordem de custo. A sequência real:

  • JavaScript interceptando o clique? Não — se algum listener chamasse preventDefault(), a URL não ganharia o hash. O hash mudava. Descartado.
  • Âncora não existe? document.getElementById('...') devolvia o elemento, com scroll-margin-top correto. Descartado.
  • Algum container interno roubando o scroll? Varri a página atrás de elementos com scrollHeight maior que clientHeight: nenhum scroller interno. O scroll era do viewport mesmo. Descartado.
  • Algo “prendendo” a posição? Aqui esquentou: window.scrollTo(0, 2000) programático… não movia um pixel. Mas a roda do mouse rolava normalmente. Scroll de usuário funcionava; scroll programático, não.
  • O teste decisivo: window.scrollTo({ top: 2000, behavior: 'instant' }) funcionou na hora. O mesmo comando com animação suave, morto. Ou seja: não era o scroll que estava quebrado — era a animação de scroll suave, que nascia e morria no primeiro frame.

Com isso a bisseção ficou trivial: desliguei o scroll-behavior: smooth do CSS em runtime e tudo passou a funcionar — âncora, scrollTo, índice, menu. Causa isolada.

Por que scroll-behavior: smooth + overflow-x: hidden quebram as âncoras?

O CSS do tema tinha uma combinação extremamente comum — está em milhares de sites em produção agora:

html {
    scroll-behavior: smooth;
}
body {
    overflow-x: hidden; /* corta vazamento horizontal de elementos decorativos */
}

O detalhe que quase ninguém conhece: quando o html tem overflow visível, a especificação manda o navegador propagar o overflow do body para o viewport. Com overflow-x: hidden no body, o eixo vertical computa como auto, e o viewport passa a se comportar como um scroll container derivado do body — e é nessa configuração que o Chromium cancela a animação de scroll suave do viewport no nascimento. A URL muda (isso é histórico de navegação), mas a rolagem, que dependia da animação, nunca acontece. Firefox e Safari têm implementações próprias; o comportamento que eu medi e descrevo aqui é o do Chromium — que, na prática, é a maioria do seu tráfego.

É um bug de interação entre duas regras corretas. Cada linha, isolada, é legítima e recomendada por aí. Juntas, no navegador dominante, matam uma função básica do HTML que existe desde os anos 90.

A correção em duas linhas (e por que clip é melhor que hidden)

body {
    overflow-x: hidden; /* fallback para navegadores antigos */
    overflow-x: clip;
}

overflow-x: clip corta o conteúdo que vaza — exatamente como hidden — mas com uma diferença estrutural: clip não cria um scroll container. Sem scroll container acidental, sem propagação esquisita para o viewport, sem animação cancelada. O suporte é amplo desde 2022 (Chrome 90+, Firefox 81+, Safari 16+), e a declaração duplicada acima é o padrão de fallback: navegador velho usa hidden, navegador atual usa clip.

E o scroll suave? Eu decidi removê-lo em vez de mantê-lo consertado. Três motivos: pulo instantâneo é 100% confiável em qualquer navegador; quem ativa “reduzir movimento” no sistema operacional já deveria receber pulo instantâneo de qualquer forma; e quinze dias de âncoras mortas me curaram da estética de rolagem animada. Suave que quebra perde para instantâneo que funciona, todos os dias da semana.

A lição de QA: feature nova denuncia bug velho

A parte que eu levo para todo projeto — inclusive os seis recursos de modernização que apliquei neste tema — é o processo, não o CSS:

  • Teste comportamento, não markup. Meu HTML estava perfeito: âncoras geradas, ids corretos, schema validando. Todos os meus checks automatizados via curl passavam — porque nenhum deles clicava. Bug de interação só aparece em navegador de verdade.
  • Feature nova é auditoria grátis do que já existia. O índice automático não criou o defeito; criou o primeiro motivo para alguém clicar numa âncora e reparar. Cada entrega nova exercita caminhos velhos — aproveite o momento.
  • Bug silencioso se mede pela ausência. Nenhuma métrica minha gritou. O visitante que clicava em “FAQ” no menu e não ia para lugar nenhum simplesmente rolava a página à mão… ou ia embora. Esse custo não aparece em dashboard nenhum.
  • Checklist atualizado: clicar toda âncora do menu e do índice em Chrome real agora faz parte da minha entrega de site — junto com formulário, 404 e redirects.

O que isso significa para o seu negócio

Se você tem um site institucional com menu do tipo “Serviços”, “Sobre”, “Contato” apontando para seções da mesma página — o formato da maioria dos sites de empresa — há uma chance real de que os cliques não estejam levando ninguém a lugar nenhum, agora, no navegador mais usado do país. O teste leva trinta segundos: abra seu site no Chrome, clique num item do menu que aponta para uma seção, e veja se a página rola de verdade ou se só a URL muda. No celular, onde um site que funciona de ponta a ponta é o mínimo que se deve exigir ao contratar, o custo desse silêncio é ainda maior: usuário mobile não investiga — desiste.

Fica a pergunta honesta: quando foi a última vez que alguém clicou em cada link do seu site num navegador de verdade — não numa ferramenta de auditoria, num navegador? Se a resposta for “não sei”, me deixa teu WhatsApp aqui embaixo. Eu faço esse teste no seu site, te mando o que encontrei — incluindo se as suas âncoras estão vivas — e você decide o que fazer com isso. Sem custo e sem compromisso.