| name | lenis |
| description | Use when implementing Lenis smooth scroll or integrating Lenis with GSAP ScrollTrigger. Applies Lenis best practices for setup and performance. |
| version | 1.2.0 |
Lenis Smooth Scroll Best Practices
Apply when implementing Lenis smooth scrolling.
Documentation: https://lenis.darkroom.engineering/
Required CSS
Always include this CSS for proper functionality:
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;
}
.lenis.lenis-scrolling iframe {
pointer-events: none;
}
Basic Setup
Standalone (Simple)
import Lenis from 'lenis';
const lenis = new Lenis({
autoRaf: true, // Uses built-in RAF loop
});
With GSAP ScrollTrigger (Recommended)
import Lenis from 'lenis';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
let lenisInstance: Lenis | null = null;
let tickerCallback: ((time: number) => void) | null = null;
function init() {
lenisInstance = new Lenis();
// Sync Lenis scroll position with ScrollTrigger
lenisInstance.on('scroll', ScrollTrigger.update);
// Store callback reference for cleanup
tickerCallback = (time) => lenisInstance?.raf(time * 1000);
gsap.ticker.add(tickerCallback);
// Disable GSAP lag smoothing for precise sync
gsap.ticker.lagSmoothing(0);
}
function destroy() {
// Remove ticker callback first (prevents memory leak!)
if (tickerCallback) {
gsap.ticker.remove(tickerCallback);
tickerCallback = null;
}
if (lenisInstance) {
lenisInstance.destroy();
lenisInstance = null;
}
}
Critical: Store the ticker callback reference. Without gsap.ticker.remove(), callbacks accumulate on each init, causing memory leaks and performance issues.
Configuration Options
const lenis = new Lenis({
duration: 1.2, // Scroll animation duration
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), // Ease out expo
orientation: 'vertical', // or 'horizontal'
gestureOrientation: 'vertical',
smoothWheel: true, // Smooth wheel scrolling
syncTouch: false, // Don't smooth touch (usually unwanted)
touchMultiplier: 2, // Touch scroll speed
});
Recommended Defaults
duration: 1.2— Smooth but responsivesmoothWheel: true— Essential for smooth feelsyncTouch: false— Native touch usually better
Preventing Scroll on Elements
<!-- Prevent Lenis on scrollable containers -->
<div data-lenis-prevent>
<div class="scrollable-content">
<!-- This scrolls normally, Lenis ignores it -->
</div>
</div>
<!-- Prevent only wheel events -->
<div data-lenis-prevent-wheel>
<!-- Touch still uses Lenis -->
</div>
<!-- Prevent only touch events -->
<div data-lenis-prevent-touch>
<!-- Wheel still uses Lenis -->
</div>
Scroll Methods
// Scroll to element
lenis.scrollTo('#section', {
offset: -100, // Offset from target
duration: 2, // Override default duration
easing: (t) => t, // Custom easing
immediate: false, // Skip animation
lock: true, // Prevent user scroll during animation
});
// Scroll to position
lenis.scrollTo(500); // Scroll to 500px
lenis.scrollTo('top'); // Scroll to top
lenis.scrollTo('bottom'); // Scroll to bottom
// Control
lenis.stop(); // Pause scrolling
lenis.start(); // Resume scrolling
Event Handling
lenis.on('scroll', ({ scroll, velocity, direction, progress }) => {
// scroll: current scroll position
// velocity: scroll speed
// direction: 1 (down) or -1 (up)
// progress: 0-1 scroll progress
});
Cleanup
// On component unmount:
lenis.destroy();
Known Limitations
- Iframes — Smooth scroll stops over iframes (they don't forward wheel events)
- Safari pre-M1 —
position: fixedmay lag slightly - Nested scroll containers — Use
data-lenis-preventon them - Browser smooth scroll — Disable any built-in smooth scrolling
Troubleshooting
Scroll not smooth
- Check required CSS is included
- Verify
autoRaf: trueor GSAP ticker integration - Check for conflicting scroll libraries
ScrollTrigger not syncing
- Ensure
lenis.on('scroll', ScrollTrigger.update)is set - Call
ScrollTrigger.refresh()after Lenis init - Check
gsap.ticker.lagSmoothing(0)is set
Elements not receiving scroll
- Check for
data-lenis-preventon parent elements - Verify Lenis is initialized after DOM ready
Common Patterns
Anchor Links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', (e) => {
e.preventDefault();
const target = document.querySelector(anchor.getAttribute('href'));
if (target) {
lenis.scrollTo(target, { offset: -80 });
}
});
});
Stop During Modal
function openModal() {
lenis.stop();
// show modal
}
function closeModal() {
lenis.start();
// hide modal
}
Avoid
- Forgetting required CSS (causes jittery scroll)
- Using
syncTouch: true(native touch is usually better) - Multiple Lenis instances on the same page
- Forgetting to call
lenis.destroy()on unmount - Using both
autoRafand GSAP ticker (pick one) - Missing
ScrollTrigger.updatesync when using GSAP - Animating scroll position directly (use
lenis.scrollTo()) - Setting
durationtoo high (>2s feels sluggish)