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

Sumário
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.
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:
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ção | Padrão | Para que serve |
|---|---|---|
syncTouch | false | Sincroniza a rolagem em touch. Instável no iOS < 16 |
syncTouchLerp | 0.075 | Intensidade do lerp na inércia sincronizada |
touchInertiaExponent | 1.7 | Força da inércia no toque |
touchMultiplier | 1 | Sensibilidade do evento de toque |
anchors | — | Ativa 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.
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ção | Resposta |
|---|---|
| Só âncoras suaves | scroll-behavior: smooth, sem biblioteca |
Página com scroll-snap | Lenis só com o pacote de snap dele |
| Landing longa, visual contínuo | Lenis se justifica |
| Documentação, blog, texto denso | Provavelmente não |
| Touch como prioridade | Deixar 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.
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.
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,
stickye âncora sobrevivem. scroll-snapdo CSS quebra. Existe pacote oficial de snap por causa disso.syncTouchjá vem desligado, eallowNestedScrolldeve 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
Motions premium em Next.js: os 18 padrões do site da Plantica, passo a passo
O scroll vira CSS custom property e o CSS faz o resto: os 18 padrões de motion do site da Plantica em Next.js, com o prompt de implementação pronto de cada um.
- motion
- gsap
Motion •
INP e animação: o que realmente conta para a métrica
Rolagem não entra no INP. O que entra é a animação rodando no momento do clique. A tabela do que toca cada fase, e os 50% contra 1% de quadros.
- inp
- core web vitals
Motion •
prefers-reduced-motion: motion acessível sem matar o design
A palavra da especificação é reduzir, não remover. Por que animation: none !important erra dos dois lados, e a tabela de substituição que preserva o design.
- acessibilidade
- motion design


