Lenis: when smooth scroll helps and when it gets in the way
The accessibility critique of smooth scroll is real and almost none of it applies to Lenis. What it breaks for real is another thing: scroll-snap.
- lenis
- smooth scroll

Contents
The most repeated critique of smooth scroll is correct and describes something else.
It was born from classic scroll hijacking: faking a fixed container and moving it with JavaScript. That broke the scrollbar, the keyboard arrows and screen reader navigation, and the reputation stuck.
Lenis does none of that. It applies easing to the native scroll position, and its own README says it wraps the browser's scroll, keeping position: sticky, anchor links and accessibility working.
Except that it charges in three other places, and none of them appears in the tutorials. One of them is a CSS feature that stops working outright, to the point where the project publishes a package of its own to replace it.
This post separates the old critique from the real costs, and ends in a decision tree. It continues the scroll foundation, now through the question that text never asks: do you need this?
What classic hijacking used to break
The old technique worked like this: lock the page body, create a container that behaves as fixed, and move that container with transform as the mouse wheel turns.
The visual result was good. The rest was not.
The scrollbar stopped reflecting the real position, because the page never scrolled for real. The keyboard arrows and the page keys lost their effect, since the browser had no scrolling to execute. Assistive technology got lost for the same reason: the browser's accessibility model follows native scrolling, and there was none there.
That technique is where the objection comes from, and it was fair. The point of this post is that it turned into a general rule about "smooth scroll" while describing one specific implementation.
What Lenis does differently
The difference is architectural, not a matter of tuning.
Per the repository itself, Lenis "runs on top of native scroll" and "wraps the browser's own scroll, so position: sticky, anchor links and accessibility keep working" (the Lenis repository, read 29 August 2026). The version referenced there is 1.3.26.
In practice, it intercepts the scroll event and writes the position back with smoothing, instead of preventing the scroll and drawing the movement on its own. The browser stays the one scrolling.
An honest caveat, because the difference between the two columns tends to turn into exaggeration. The verifiable part is that classic hijacking's failure modes fail to apply: the scrollbar, keyboard, anchors and sticky keep working. That differs from saying the library passed an accessibility audit, and anyone claiming "no trade-off at all" is claiming more than the documentation supports.
Anyone wanting to do the right thing in full has one extra step, and it is cheap: switching the whole library off for people who asked for less movement. That is the subject of accessible motion, and it counts for more than any easing adjustment.
One CSS detail accompanies that decision and seldom gets explained. The pair of rules below, appearing in close to every project with Lenis, switches off the native scroll-behavior while the library is active:
html { scroll-behavior: smooth; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }The reason is that both do the same thing by different paths. With native smooth on at the same time, a click on an anchor fires the browser's animation and the library's, and the result is a movement that accelerates twice or backtracks. The rule exists so that one of them alone stays in command.
The practical consequence is good: when you switch Lenis off under prefers-reduced-motion, the lenis-smooth class never gets applied, the native scroll-behavior: smooth comes back into force, and the anchor stays smooth with no library at all. The graceful degradation is free.
What breaks: CSS scroll-snap
This is the concrete cost, and it has proof in the repository itself.
With scroll-snap active, the sections jump with no animation instead of gliding. The project's issue #12 reports the behavior, and the stronger evidence is another: Lenis publishes a snap package of its own, which exists for the single reason that the CSS one fails to work alongside. A library that ships a replacement for a native feature is admitting the incompatibility.
The explanation circulating around the web is wrong, and undoing it pays off. People say "Lenis hijacks the scroll and snap expects native behavior", and the README states the opposite, and the README is right: it runs on top of native scrolling.
The real mechanism is another. scroll-snap works by discrete settling: when the scroll ends, the browser picks a snap point and moves to it. Lenis applies continuous easing: it is always adjusting the position to reach the destination with a smooth landing. Both want to decide where the scroll stops, and one cancels the other.
The way out is choosing. Either you use native snap and give up the easing, or you adopt Lenis's snap package and start controlling the settling through the library.
What it costs: touch and DOM sweeping
Two options deserve attention, and both ship with the right default.
syncTouch ships off. It is the option that synchronizes scrolling on touch devices, imitating native behavior. The README warns that it "can be unstable on iOS below version 16". The false default means that, on a phone, scrolling stays the system's, with the hardware inertia the device already delivers. Turning that option on is a decision, not a detail.
| Option | Default | What it does |
|---|---|---|
syncTouch | false | Synchronizes scrolling on touch. Unstable on iOS < 16 |
syncTouchLerp | 0.075 | Lerp intensity in the synchronized inertia |
touchInertiaExponent | 1.7 | Inertia strength on touch |
touchMultiplier | 1 | Sensitivity of the touch event |
anchors | — | Enables handling of native anchor links |
allowNestedScroll is the trap. It exists to allow scrolling inside nested elements, and the documentation warns that turning it on "can create performance issues, because it checks the DOM tree on every scroll event". The official recommendation is using the prevent option instead, marking the elements that should stay out.
The difference between the two is cost per event: prevent decides once, per element; allowNestedScroll decides every time, walking the tree.
The problem every project meets: a scrollable area inside the page
As soon as the site gains a modal, a long dropdown or a search palette, the same symptom appears: the person scrolls inside the box, and the whole page scrolls behind it. Or worse, only the page scrolls and the box does not.
It happens because Lenis intercepts the scroll event on the document, and knows nothing about that element having a scroll of its own.
The recommended solution is marking the element with data-lenis-prevent. The attribute tells the library to leave events originating inside it alone, giving that box back its native behavior. It is a per-element decision, made once, and that is why the documentation prefers it over allowNestedScroll.
The symptom is easy to recognize and hard to attribute: it looks like a modal bug, and it is scroll configuration. If you adopt Lenis, mark every area with a scroll of its own from the start, meaning a modal, a drawer, an autocomplete, a table with horizontal scrolling.
The wrong objection: Core Web Vitals
Here is the argument that appears in every discussion about smooth scroll and that fails to hold up.
Scrolling counts toward no INP. The metric watches clicks, taps and keys, and ignores scroll, hover and zoom. That comes from the definition, and what counts for the metric covers it in detail. A library acting during the scroll has no direct way to worsen INP, because during the scroll no interaction is under measurement.
That absolves the library of nothing. It means anyone wanting to decide with data has to look at the three costs on the right, and not at a metric that never sees the problem.
When native CSS is enough
A good share of the cases needs no library at all.
If what you want is a click on an anchor link gliding to the section, scroll-behavior: smooth solves it. It is one line of CSS, with no package, no initialization, no conflict with scroll-snap, and it respects prefers-reduced-motion once you switch it off inside the media query.
Native CSS fails to apply easing to continuous scrolling, the kind a person does with the mouse wheel or a finger. That is the one thing Lenis delivers and the browser does not.
So the question separating the two cases is short: do you want to smooth the jump to an anchor or the whole scroll? Only the second justifies the library.
If the goal is animating elements as the page scrolls, that is another subject with a native solution of its own, covered in scroll animation with no JavaScript. Smooth scrolling and scroll-triggered animation are different things, and confusing them is the reason for plenty of unnecessary installs.
The decision tree
<!-- [UNIQUE INSIGHT] -->
Three questions, in order. The first eliminates most projects.
1. Do you need easing on continuous scrolling, or only a smooth anchor? If it is the anchor, scroll-behavior: smooth and you are done.
2. Does the page use scroll-snap? If it does, having both in the standard way is impossible. Either native snap, or Lenis with its own snap package.
3. Is the gain noticeable in your content? Scroll easing gets noticed on a long page with continuous visual composition. In dense text, the reader scrolls to reach the content, and the easing delays the arrival. The same criterion holds for entrance effects: text animation pays off in a hero and tires in an article.
| Situation | Answer |
|---|---|
| Smooth anchors alone | scroll-behavior: smooth, no library |
A page with scroll-snap | Lenis with its snap package alone |
| A long, visually continuous landing page | Lenis justifies itself |
| Documentation, a blog, dense text | Odds are no |
| Touch as the priority | Leave syncTouch off, as it ships |
The catalog of techniques for the cases where the answer is yes sits in the guide to motion design for the web.
If the answer is no, how to get out
Removing it is simpler than installing it, as long as the order gets respected, or residue stays behind.
First, take out the initialization and the ticker. If an integration with ScrollTrigger exists, the two lines connecting them go too, or GSAP keeps calling a loop that no longer exists.
Second, delete the CSS. The html.lenis, .lenis.lenis-smooth and html.lenis-stopped rules become dead code, and the second is the one that fools people most: while it exists with scroll-behavior: auto !important, native smooth stays off even with no library. It is the classic case of an anchor that "stopped working" after a half-finished removal.
Third, decide about data-lenis-prevent. The attributes go inert and break nothing, and they become a false trail for whoever reads the code later.
Last, take the dependency out of package.json. Leaving the package installed and unused adds nothing to the final bundle, since nothing imports it, and it keeps the illusion that the site uses the library, which is how it comes back in the next refactor.
Lenis with ScrollTrigger
The combined use is the most common, and it holds a synchronization trap.
Once Lenis controls the scroll position, ScrollTrigger has to know about it, or the triggers fire at the wrong positions. The connection takes two lines: Lenis notifies ScrollTrigger on every event, and GSAP's ticker starts calling Lenis's loop.
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);lagSmoothing(0) matters more than it looks. Without it, GSAP tries to compensate for dropped frames by skipping time, and the result is the animation and the scroll disagreeing about where the page is.
The patterns that work after that connection sit in the eight ScrollTrigger patterns. And the decision that comes before that one, about which animation tool to use, sits in GSAP, Motion or pure CSS.
How this blog decides
<!-- [PERSONAL EXPERIENCE] -->
ZUMKAI uses Lenis, and the real configuration lives in the repository. I show it here, because it answers three of this post's questions without my having to theorize.
The initialization sits in components/motion-provider.tsx, and the first line is the one that matters most: Lenis gets created only when the person asked for no reduction in movement. The animation is not what gets switched off under prefers-reduced-motion, it is the whole library that never comes into existence.
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);
}Two absences say as much as the lines that are there. syncTouch never gets passed, so it stays at the off default. And allowNestedScroll never does either: in its place, the Cmd+K search list carries data-lenis-prevent, which is the prevent option the documentation recommends, to the letter.
Now the uncomfortable part. By this post's decision tree, a blog of dense text lands in "odds are no". Justifying Lenis here has nothing to do with the articles: it is the case and portfolio pages, which carry parallax and a hero zoom. The library loads across the whole site because of one part of it.
The explicit judgment: that is a debt, and no elegant decision. The correct way out would be loading the motion only on the routes that use it. It is on the list, and while it stays undone, the cost is real and pretending the tidy configuration makes up for it helps nobody.
Frequently asked questions
Does smooth scroll hurt accessibility?
It depends on the implementation. Classic scroll hijacking, which locks the page and moves a container with transform, breaks the scrollbar, the keyboard and assistive technology. Lenis applies easing to the native position, and the README states that position: sticky, anchors and accessibility keep working. Switching the library off under prefers-reduced-motion stays mandatory.
Is Lenis worth it?
It is worth it when you need easing on continuous scrolling, the page uses no scroll-snap, and the content carries enough visual continuity for the effect to register. To smooth the jump to an anchor alone, CSS scroll-behavior: smooth solves it with no package at all.
Why did my scroll-snap stop working?
Because Lenis's continuous easing and scroll-snap's discrete settling compete over the same decision: where the scroll ends. The repository's issue #12 reports the behavior, and the project publishes a snap package of its own for the case. Either you use native snap without Lenis, or the library's snap.
Is scroll-behavior: smooth not enough?
It is enough for anchors. It smooths the jump when someone clicks an internal link, in one line of CSS. It fails to apply easing to continuous scrolling done with a mouse wheel or a finger, which is the one thing Lenis adds.
Does it hurt Core Web Vitals?
Not through INP, which is the metric cited in that objection. INP measures clicks, taps and keys, and ignores scrolling. Lenis's real costs are others: the conflict with scroll-snap, the touch processing if you turn syncTouch on, and one more package in the bundle.
Does it work well on a phone?
By default it interferes with touch scrolling in no way: syncTouch ships off, and the device keeps using native inertia. The README warns that turning that option on can be unstable on iOS below version 16, so the decision to enable it needs testing on a real device.
What to take away
- The accessibility critique describes classic hijacking, not Lenis.
- Lenis runs on top of native scrolling: the scrollbar, keyboard,
stickyand anchors survive. - CSS
scroll-snapbreaks. An official snap package exists because of it. syncTouchalready ships off, andallowNestedScrollshould stay off.- The Core Web Vitals objection is the wrong one: scrolling counts toward no INP.
- For a smooth anchor, one line of CSS solves it.
Before installing, answer the tree's first question: do you want to smooth the anchor or the whole scroll? If it is the anchor, you have saved a package, an initialization and a conflict with scroll-snap.
Read next
Motion •
Motion Design for the Web: The Complete Guide
Scroll, text, images and video: the complete catalog of motion techniques for the web, with implementation in Next.js and the cases where each one pays off.
- motion
- scroll
The definitive guide — a Next.js site built around motion and scroll
The scroll foundation that, when missing, keeps the animations from working at all: Lenis, GSAP and Next.js wired in the right order and the mistakes to avoid.
- next.js
- lenis
Infra •
Documentation: deploying a Next.js application with GitHub + Hostinger
Every push becomes a live site with no hosting panel involved: connecting GitHub to Hostinger, the build settings that break and the checks after each deploy.
- deploy
- github


