Pular para o conteúdo
Zumkai

Lenis: quando o smooth scroll ajuda e quando atrapalha

A crítica de acessibilidade ao smooth scroll é real e quase toda ela não se aplica ao Lenis. O que ele quebra de verdade é outra coisa: o scroll-snap.

  • lenis
  • smooth scroll
Card contrastando o que o Lenis preserva — barra, teclado, sticky e âncora — com o que ele quebra: o scroll-snap do CSS.
Sumário
  1. O que o hijacking clássico quebrava
  2. O que o Lenis faz de diferente
  3. O que quebra: o scroll-snap do CSS
  4. O que custa: touch e varredura de DOM
  5. A objeção errada: Core Web Vitals
  6. Quando o CSS nativo basta
  7. A árvore de decisão
  8. Lenis com ScrollTrigger
  9. Como este blog decide
  10. Perguntas frequentes
  11. O que levar

A crítica mais repetida ao smooth scroll está correta e descreve outra coisa.

Ela nasceu do scroll-hijacking clássico: fingir um contêiner fixo e movê-lo com JavaScript. Isso quebrava a barra de rolagem, as setas do teclado e a navegação por leitor de tela, e a fama ficou.

O Lenis não faz isso. Ele aplica easing à posição nativa de rolagem, e o próprio README diz que envolve o scroll do navegador, mantendo position: sticky, links de âncora e acessibilidade funcionando.

Só que ele cobra em outros três lugares, e nenhum deles aparece nos tutoriais. Um deles é um recurso do CSS que simplesmente para de funcionar — a ponto de o projeto publicar um pacote próprio para substituí-lo.

Este post separa a crítica velha dos custos reais, e termina numa árvore de decisão. É a continuação de a fundação de scroll, agora pela pergunta que aquele texto não faz: você precisa disso?

O que o hijacking clássico quebrava

A técnica antiga funcionava assim: travar o corpo da página, criar um contêiner que se comporta como fixo, e mover esse contêiner com transform conforme a roda do mouse gira.

O resultado visual era bom. O resto, não.

A barra de rolagem deixava de refletir a posição real, porque a página não rolava de verdade. As setas do teclado e as teclas de página perdiam o efeito, já que o navegador não tinha rolagem para executar. Tecnologia assistiva se perdia pelo mesmo motivo: o modelo de acessibilidade do navegador acompanha a rolagem nativa, e ali não havia nenhuma.

É dessa técnica que vem a objeção, e ela era justa. O ponto do post é que ela virou regra geral sobre "smooth scroll" quando descrevia uma implementação específica.

O que o Lenis faz de diferente

A diferença é de arquitetura, não de ajuste fino.

Segundo o próprio repositório, o Lenis "roda sobre o scroll nativo" e "envolve o scroll do próprio navegador, então position: sticky, links de âncora e acessibilidade continuam funcionando" (repositório do Lenis, lido em 29/08/2026). A versão referenciada ali é a 1.3.26.

Na prática, ele intercepta o evento de rolagem e escreve a posição de volta com suavização, em vez de impedir a rolagem e desenhar o movimento por conta própria. O navegador continua sendo quem rola.

Duas formas de fazer rolagem suave No hijacking clássico a página é travada e um contêiner é movido com transform, o que quebra barra de rolagem, teclado e tecnologia assistiva. No Lenis, o easing é aplicado à posição nativa de rolagem, então barra, teclado, sticky e âncora continuam funcionando. hijacking clássico trava a página move contêiner com transform barra não reflete a posição teclado sem efeito tecnologia assistiva perdida Lenis o navegador continua rolando easing sobre a posição nativa barra reflete a posição teclado e âncora funcionam `position: sticky` preservado A crítica antiga descreve a caixa da esquerda
Fonte: README do repositório do Lenis. A coluna da esquerda descreve a técnica que originou a objeção de acessibilidade.

Uma ressalva de honestidade, porque a diferença entre as duas colunas costuma virar exagero. O que é verificável é que os modos de falha do hijacking clássico não se aplicam: barra, teclado, âncora e sticky continuam. Isso não é o mesmo que dizer que a biblioteca passou por auditoria de acessibilidade, e quem afirma "sem trade-off nenhum" está afirmando mais do que a documentação sustenta.

Quem quiser fazer a coisa certa por completo tem um passo adicional, e ele é barato: desligar a biblioteca inteira para quem pediu menos movimento. É o assunto de motion acessível, e vale mais que qualquer ajuste de easing.

Há um detalhe de CSS que acompanha essa decisão e quase nunca é explicado. O par de regras abaixo, que aparece em praticamente todo projeto com Lenis, desliga o scroll-behavior nativo enquanto a biblioteca está ativa:

css
html { scroll-behavior: smooth; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }

A razão é que os dois fazem a mesma coisa por caminhos diferentes. Com o smooth nativo ligado ao mesmo tempo, um clique em âncora dispara a animação do navegador e a da biblioteca, e o resultado é um movimento que acelera duas vezes ou volta atrás. A regra existe para que só um deles esteja no comando.

A consequência prática é boa: quando você desliga o Lenis sob prefers-reduced-motion, a classe lenis-smooth não é aplicada, o scroll-behavior: smooth nativo volta a valer, e a âncora continua suave sem biblioteca nenhuma. O degradê é gratuito.

O que quebra: o scroll-snap do CSS

Este é o custo concreto, e tem prova no próprio repositório.

Com scroll-snap ativo, as seções pulam sem animação em vez de rolar suavemente. O comportamento está relatado na issue #12 do projeto, e a evidência mais forte é outra: o Lenis publica um pacote próprio de snap, que existe justamente porque o do CSS não funciona junto. Quando uma biblioteca entrega substituto para um recurso nativo, ela está admitindo a incompatibilidade.

A explicação que circula pela web está errada, e convém desfazê-la. Dizem que "o Lenis sequestra a rolagem e o snap espera comportamento nativo" — mas o README afirma o contrário, e é verdade: ele roda sobre a rolagem nativa.

O mecanismo real é outro. O scroll-snap funciona por assentamento discreto: quando a rolagem termina, o navegador escolhe um ponto de encaixe e move até ele. O Lenis aplica easing contínuo: ele está sempre ajustando a posição para chegar suavemente ao destino. As duas coisas querem decidir onde a rolagem para, e uma anula a outra.

A saída é escolher. Ou você usa o snap nativo e abre mão do easing, ou adota o pacote de snap do Lenis e passa a controlar o encaixe pela biblioteca.

O que custa: touch e varredura de DOM

Duas opções merecem atenção, e as duas já vêm com o padrão correto.

syncTouch vem desligado. É a opção que sincroniza a rolagem em dispositivos de toque, imitando o comportamento nativo. O README avisa que ela "pode ser instável no iOS abaixo da versão 16". O padrão false significa que, em celular, a rolagem continua sendo a do sistema, com a inércia de hardware que o aparelho já entrega. Ligar essa opção é uma decisão, não um detalhe.

OpçãoPadrãoPara que serve
syncTouchfalseSincroniza a rolagem em touch. Instável no iOS < 16
syncTouchLerp0.075Intensidade do lerp na inércia sincronizada
touchInertiaExponent1.7Força da inércia no toque
touchMultiplier1Sensibilidade do evento de toque
anchorsAtiva o tratamento de links de âncora nativos

allowNestedScroll é a armadilha. Ela existe para permitir rolagem dentro de elementos aninhados, e a documentação avisa que ligá-la "pode criar problemas de performance, porque verifica a árvore do DOM a cada evento de rolagem". A recomendação oficial é usar a opção prevent no lugar, marcando os elementos que devem ficar de fora.

A diferença entre as duas é de custo por evento: prevent decide uma vez, por elemento; allowNestedScroll decide sempre, percorrendo a árvore.

O problema que todo projeto encontra: área rolável dentro da página

Assim que o site ganha um modal, um menu suspenso longo ou uma paleta de busca, aparece o mesmo sintoma: a pessoa rola dentro da caixa, e a página inteira rola junto por trás. Ou pior, só a página rola e a caixa não.

Acontece porque o Lenis intercepta o evento de rolagem no documento, e não sabe que aquele elemento tem rolagem própria.

A solução recomendada é marcar o elemento com data-lenis-prevent. O atributo diz à biblioteca para não interceptar eventos originados ali dentro, devolvendo o comportamento nativo àquela caixa. É uma decisão por elemento, tomada uma vez, e é por isso que a documentação a prefere no lugar de allowNestedScroll.

O sintoma é fácil de reconhecer e difícil de atribuir: parece bug do modal, e é configuração da rolagem. Se você adotar Lenis, marque desde o começo toda área com rolagem própria — modal, gaveta, autocompletar, tabela com rolagem horizontal.

A objeção errada: Core Web Vitals

Aqui está o argumento que aparece em toda discussão sobre smooth scroll e que não se sustenta.

Rolagem não conta para o INP. A métrica observa clique, toque e tecla, e ignora scroll, hover e zoom. Isso vem da definição, e está detalhado em o que realmente conta para a métrica. Uma biblioteca que atua durante a rolagem não tem como piorar diretamente o INP, porque durante a rolagem não existe interação sendo medida.

Onde o custo do Lenis está e onde não está O INP mede clique, toque e tecla, e não mede rolagem, então o Lenis não afeta essa métrica diretamente. Os custos reais são o conflito com scroll-snap, o processamento em touch quando syncTouch é ligado e o peso do pacote no bundle. A objeção que não se sustenta “smooth scroll piora o INP” rolagem não é interação medida — a métrica não vê Os três custos reais scroll-snap para de funcionar touch só se você ligar bundle mais um pacote
O argumento popular aponta para a métrica errada. Os custos existem, em outros três lugares.

Isso não absolve a biblioteca. Significa que quem quer decidir com dado precisa olhar para os três custos da direita, e não para uma métrica que nem enxerga o problema.

Quando o CSS nativo basta

Boa parte dos casos não precisa de biblioteca nenhuma.

Se o que você quer é que um clique em link de âncora role suavemente até a seção, scroll-behavior: smooth resolve. É uma linha de CSS, não tem pacote, não tem inicialização, não conflita com scroll-snap e respeita prefers-reduced-motion quando você o desliga dentro da media query.

O que o CSS nativo não faz é aplicar easing à rolagem contínua, aquela que a pessoa faz com a roda do mouse ou com o dedo. Essa é a única coisa que o Lenis entrega e o navegador não.

Então a pergunta que separa os dois casos é curta: você quer suavizar o pulo até uma âncora ou a rolagem inteira? Só a segunda justifica a biblioteca.

Se o objetivo for animar elementos conforme a página rola, isso é outro assunto e tem solução nativa própria, tratada em animação de scroll sem JavaScript. Rolagem suave e animação disparada por rolagem são coisas diferentes, e confundi-las é o motivo de muita instalação desnecessária.

A árvore de decisão

<!-- [UNIQUE INSIGHT] -->

Três perguntas, na ordem. A primeira elimina a maioria dos projetos.

1. Você precisa de easing na rolagem contínua, ou só de âncora suave? Se for âncora, scroll-behavior: smooth e acabou.

2. A página usa scroll-snap? Se usa, não dá para ter os dois do jeito padrão. Ou o snap nativo, ou o Lenis com o pacote de snap dele.

3. O ganho é perceptível no seu conteúdo? Easing de rolagem se percebe em página longa com composição visual contínua. Em texto denso, o leitor rola para chegar ao conteúdo, e o easing atrasa a chegada. O mesmo critério vale para efeitos de entrada: animação de texto rende num hero e cansa num artigo.

SituaçãoResposta
Só âncoras suavesscroll-behavior: smooth, sem biblioteca
Página com scroll-snapLenis só com o pacote de snap dele
Landing longa, visual contínuoLenis se justifica
Documentação, blog, texto densoProvavelmente não
Touch como prioridadeDeixar syncTouch desligado, como já vem

O catálogo de técnicas para os casos em que a resposta é sim está em o guia de motion design para web.

Se a resposta for não, como sair

Remover é mais simples do que instalar, desde que a ordem seja respeitada — senão fica resíduo.

Primeiro, tire a inicialização e o ticker. Se houver integração com ScrollTrigger, as duas linhas que ligam os dois saem junto, ou o GSAP fica chamando um loop que não existe mais.

Segundo, apague o CSS. As regras html.lenis, .lenis.lenis-smooth e html.lenis-stopped viram código morto, e a segunda é a que mais engana: enquanto ela existir com scroll-behavior: auto !important, o smooth nativo continua desligado mesmo sem a biblioteca. É o caso clássico de âncora que "parou de funcionar" depois de uma remoção pela metade.

Terceiro, decida sobre o data-lenis-prevent. Os atributos ficam inertes e não quebram nada, mas viram pista falsa para quem ler o código depois.

Por último, tire a dependência do package.json. Deixar o pacote instalado sem uso não pesa no bundle final, já que nada o importa, mas mantém a ilusão de que o site usa a biblioteca — e é assim que ela volta na próxima refatoração.

Lenis com ScrollTrigger

O uso combinado é o mais comum, e tem uma armadilha de sincronia.

Quando o Lenis controla a posição da rolagem, o ScrollTrigger precisa saber disso, ou os gatilhos disparam em posições erradas. A ligação é feita em duas linhas: o Lenis avisa o ScrollTrigger a cada evento, e o ticker do GSAP passa a chamar o loop do Lenis.

js
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);

lagSmoothing(0) importa mais do que parece. Sem ele, o GSAP tenta compensar quedas de quadro pulando o tempo, e o resultado é a animação e a rolagem discordarem sobre onde a página está.

Os padrões que funcionam depois dessa ligação estão em os oito padrões de ScrollTrigger. E a decisão anterior a essa, sobre qual ferramenta de animação usar, está em GSAP, Motion ou CSS puro.

Como este blog decide

<!-- [PERSONAL EXPERIENCE] -->

O ZUMKAI usa Lenis, e a configuração real está no repositório. Mostro ela aqui, porque responde três das perguntas deste post sem eu precisar teorizar.

A inicialização fica em components/motion-provider.tsx, e a primeira linha é a que mais importa: o Lenis só é criado quando a pessoa não pediu menos movimento. Não é a animação que é desligada sob prefers-reduced-motion — é a biblioteca inteira que nem chega a existir.

js
if (!reduced) {
  lenis = new Lenis({
    duration: 1.1,
    easing: (t) => 1 - Math.pow(2, -10 * t),
  });
  lenis.on("scroll", ScrollTrigger.update);
  gsap.ticker.add((time) => lenis?.raf(time * 1000));
  gsap.ticker.lagSmoothing(0);
}

Duas ausências dizem tanto quanto o que está escrito. syncTouch não é passado, então fica no padrão desligado. E allowNestedScroll também não: no lugar dele, a lista da busca por Cmd+K carrega data-lenis-prevent, que é exatamente a opção prevent que a documentação recomenda.

Agora a parte incômoda. Pela árvore de decisão deste post, um blog de texto denso cai em "provavelmente não". O que justifica o Lenis aqui não são os artigos: são as páginas de caso e de portfólio, que têm parallax e zoom de hero. A biblioteca é carregada em todo o site por causa de uma parte dele.

O julgamento explícito: isso é uma dívida, não uma decisão elegante. A saída correta seria carregar o motion só nas rotas que o usam. Está na lista, e enquanto não estiver feito, o custo é real e não adianta fingir que a configuração bonita compensa.

Perguntas frequentes

Smooth scroll atrapalha acessibilidade?

Depende da implementação. O scroll-hijacking clássico, que trava a página e move um contêiner com transform, quebra barra de rolagem, teclado e tecnologia assistiva. O Lenis aplica easing à posição nativa, e o README afirma que position: sticky, âncoras e acessibilidade continuam funcionando. Desligar a biblioteca sob prefers-reduced-motion continua sendo obrigatório.

Lenis vale a pena?

Vale quando você precisa de easing na rolagem contínua, a página não usa scroll-snap e o conteúdo é visualmente contínuo o bastante para o efeito ser percebido. Para suavizar só o pulo até uma âncora, scroll-behavior: smooth do CSS resolve sem pacote nenhum.

Por que meu scroll-snap parou de funcionar?

Porque o easing contínuo do Lenis e o assentamento discreto do scroll-snap disputam a mesma decisão: onde a rolagem termina. O comportamento está relatado na issue #12 do repositório, e o projeto publica um pacote de snap próprio para o caso. Ou você usa o snap nativo sem Lenis, ou o snap da biblioteca.

scroll-behavior: smooth não basta?

Basta para âncoras. Ele suaviza o salto quando alguém clica num link interno, em uma linha de CSS. O que ele não faz é aplicar easing à rolagem contínua feita com roda do mouse ou dedo, que é a única coisa que o Lenis entrega a mais.

Piora os Core Web Vitals?

Não pelo INP, que é a métrica citada nessa objeção. O INP mede clique, toque e tecla, e ignora rolagem. Os custos reais do Lenis são outros: o conflito com scroll-snap, o processamento em touch caso você ligue syncTouch, e mais um pacote no bundle.

Funciona bem no celular?

Por padrão ele não interfere na rolagem por toque: syncTouch vem desligado, e o aparelho continua usando a inércia nativa. O README avisa que ligar essa opção pode ser instável no iOS abaixo da versão 16, então a decisão de ativar precisa de teste em dispositivo real.

O que levar

  • A crítica de acessibilidade descreve o hijacking clássico, não o Lenis.
  • O Lenis roda sobre a rolagem nativa: barra, teclado, sticky e âncora sobrevivem.
  • scroll-snap do CSS quebra. Existe pacote oficial de snap por causa disso.
  • syncTouch já vem desligado, e allowNestedScroll deve continuar desligado.
  • A objeção de Core Web Vitals é a errada: rolagem não conta para o INP.
  • Para âncora suave, uma linha de CSS resolve.

Antes de instalar, responda a primeira pergunta da árvore: você quer suavizar a âncora ou a rolagem inteira? Se for a âncora, você acabou de economizar um pacote, uma inicialização e um conflito com scroll-snap.

Leia também