| name | lenis-smooth-scroll |
| description | Lenis — lightweight (3KB, MIT) smooth momentum scroll, the foundation layer of the scroll-storytelling stack. Use when implementing smooth scrolling or any scroll-driven storytelling page; always pair with GSAP ScrollTrigger (SK-044) via the GSAP-ticker sync pattern. Covers setup, ReactLenis provider, scrollTo, data-lenis-prevent, required CSS. |
| user-invocable | true |
Lenis Smooth Scroll
When to Use This Skill
Apply when implementing smooth momentum scrolling. Lenis (3KB) is the 2026 industry standard, replacing locomotive-scroll and custom implementations. Always pair with GSAP ScrollTrigger for scroll-driven animations. Auto-activate on keywords: smooth-scroll, lenis, momentum-scroll, scroll-hijack.
Installation
npm install lenis
Basic Setup
import Lenis from 'lenis';
const lenis = new Lenis({
duration: 1.2,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
orientation: 'vertical',
gestureOrientation: 'vertical',
smoothWheel: true,
wheelMultiplier: 1,
touchMultiplier: 2,
infinite: false,
});
function raf(time) {
lenis.raf(time);
requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
GSAP ScrollTrigger Integration (Primary Use Case)
import Lenis from 'lenis';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
const lenis = new Lenis();
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);
gsap.to('.hero', {
scrollTrigger: {
trigger: '.hero',
start: 'top top',
end: 'bottom top',
scrub: true,
},
y: -100,
opacity: 0,
});
React / Next.js Integration
'use client';
import { useEffect } from 'react';
import Lenis from 'lenis';
export function SmoothScroll({ children }: { children: React.ReactNode }) {
useEffect(() => {
const lenis = new Lenis();
function raf(time: number) {
lenis.raf(time);
requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
return () => lenis.destroy();
}, []);
return <>{children}</>;
}
'use client';
import { useEffect, useRef } from 'react';
import Lenis from 'lenis';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
export function useLenisGsap() {
lenisRef = useRef< | >();
( {
gsap.();
lenis = ();
lenisRef. = lenis;
lenis.(, .);
gsap..( lenis.(time * ));
gsap..();
{
lenis.();
gsap..(lenis.);
};
}, []);
lenisRef;
}
Horizontal Scroll
const lenis = new Lenis({
orientation: 'horizontal',
gestureOrientation: 'both',
wrapper: document.querySelector('.horizontal-wrapper'),
content: document.querySelector('.horizontal-content'),
});
Scroll-To (Programmatic)
lenis.scrollTo('#section-2');
lenis.scrollTo(document.querySelector('.target'));
lenis.scrollTo(500);
lenis.scrollTo('top');
lenis.scrollTo('bottom');
lenis.scrollTo('#section', {
offset: -100,
duration: 2,
easing: (t) => t,
immediate: false,
lock: false,
onComplete: () => {},
});
Events and Methods
lenis.on('scroll', ({ scroll, limit, velocity, direction, progress }) => {
console.log({ scroll, velocity, progress });
});
lenis.stop();
lenis.start();
lenis.destroy();
lenis.scroll;
lenis.progress;
lenis.velocity;
lenis.isScrolling;
lenis.direction;
CSS Required
html.lenis, html.lenis body {
height: auto;
}
.lenis.lenis-smooth {
scroll-behavior: auto !important;
}
.lenis.lenis-smooth [data-lenis-prevent] {
overscroll-behavior: contain;
}
.lenis.lenis-stopped {
overflow: hidden;
}
Performance Notes
- Lenis is 3KB gzipped — negligible bundle cost
- Uses native
scrollTo under the hood — no fake scroll containers
[data-lenis-prevent] attribute on elements that need native scroll (e.g., modals, code blocks)
- Destroy on cleanup — prevent memory leaks in SPA navigation
- Pair with
content-visibility: auto on long pages for render optimization
Common Patterns
Scroll-locked sections (snap):
gsap.to('.panel', {
scrollTrigger: {
trigger: '.panel',
start: 'top top',
pin: true,
pinSpacing: true,
snap: 1,
},
});
Infinite scroll:
const lenis = new Lenis({ infinite: true });
Disable on mobile (if needed):
const isMobile = window.matchMedia('(max-width: 768px)').matches;
const lenis = new Lenis({ smoothWheel: !isMobile });
ReactLenis Wrapper (Official)
The official React wrapper from lenis/react. Use with autoRaf: false when integrating with GSAP (SALA pattern).
'use client';
import { useEffect, useRef } from 'react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { ReactLenis } from 'lenis/react';
import type { LenisRef } from 'lenis/react';
gsap.registerPlugin(ScrollTrigger);
export function SmoothScrollProvider({ children }: { children: React.ReactNode }) {
const lenisRef = useRef<LenisRef>(null);
useEffect(() => {
function update(time: number) {
lenisRef.current?.lenis?.raf(time * 1000);
}
gsap.ticker.add(update);
gsap.ticker.lagSmoothing(0);
return () => gsap.ticker.remove(update);
}, []);
return (
< = }} =>
{children}
);
}
Critical: autoRaf: false prevents Lenis from running its own RAF loop — GSAP's ticker drives it instead (Single Animation Loop Architecture). The full SALA pattern lives in the orchestration section of gsap-advanced (SK-044), extracted from the archived cinematic-web-engine (SK-096).
useLenis Hook
Access the Lenis instance inside any component:
import { useLenis } from 'lenis/react';
function ScrollProgress() {
useLenis(({ scroll, progress, velocity }) => {
console.log({ scroll, progress, velocity });
});
return null;
}