Lenis is a lightweight, robust, and performant smooth scroll library designed by @darkroom.engineering to be simple to use and easy to integrate into your projects. This guide covers implementing Lenis in Next.js 15 with React 19 support and the latest best practices.
What is Lenis?
Lenis ("smooth" in latin) is a lightweight, robust, and performant smooth scroll library. It's designed by @darkroom.engineering to be simple to use and easy to integrate into your projects. It's built with performance in mind and is optimized for modern browsers. Key features include:
- Smooth momentum-based scrolling
- Touch device support
- Horizontal scrolling capabilities
- Scroll snapping support
- Performance optimization
- Easy integration with animation libraries
- Lightweight (~2Kb gzipped)
Installation
The package has been renamed from @studio-freight/lenis
to lenis
. Install the latest version:
1npm install lenis2# or3yarn add lenis4# or5pnpm add lenis
Important: The old @studio-freight/lenis
and @studio-freight/react-lenis
packages have been deprecated. Use the new lenis
package instead.
Basic Implementation with autoRaf
Step 1: Create a Lenis Provider Component
The latest Lenis version includes an autoRaf
option that automatically handles the requestAnimationFrame loop:
1// components/providers/lenis-provider.tsx2"use client";34import { createContext, useContext, useEffect, useRef } from "react";5import Lenis from "lenis";67import type { LenisOptions } from "lenis";89const LenisContext = createContext<Lenis | null>(null);1011export function LenisProvider({12 children,13 options = {},14}: {15 children: React.ReactNode;16 options?: LenisOptions;17}) {18 const lenisRef = useRef<Lenis | null>(null);1920 useEffect(() => {21 const lenis = new Lenis({22 autoRaf: true,23 duration: 1.2,24 easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),25 touchMultiplier: 2,26 infinite: false,27 anchors: true,28 ...options,29 });3031 lenisRef.current = lenis;3233 return () => {34 lenis.destroy();35 lenisRef.current = null;36 };37 }, [options]);3839 return (40 <LenisContext.Provider value={lenisRef.current}>41 {children}42 </LenisContext.Provider>43 );44}4546export function useLenis() {47 const context = useContext(LenisContext);48 if (context === undefined) {49 throw new Error("useLenis must be used within a LenisProvider");50 }51 return context;52}
Step 2: Required CSS
Lenis requires specific CSS for proper functionality. You can include it in two ways:
Option 1: Import via JavaScript (recommended for bundlers):
1// Import in your main component or layout2import "lenis/dist/lenis.css";
Option 2: Add to your global CSS:
1/* Add to your global CSS */2html.lenis,3html.lenis body {4 height: auto;5}67.lenis.lenis-smooth {8 scroll-behavior: auto !important;9}1011.lenis.lenis-smooth [data-lenis-prevent] {12 overscroll-behavior: contain;13}1415.lenis.lenis-stopped {16 overflow: hidden;17}1819.lenis.lenis-smooth iframe {20 pointer-events: none;21}
Step 3: Integration with Next.js 15
For App Router (Next.js 13+):
1// app/layout.tsx2import { LenisProvider } from "@/components/providers/lenis-provider";3import "./globals.css";45export default function RootLayout({6 children,7}: {8 children: React.ReactNode;9}) {10 return (11 <html lang="en">12 <body>13 <LenisProvider>{children}</LenisProvider>14 </body>15 </html>16 );17}
For Pages Router (legacy):
1// pages/_app.tsx2import type { AppProps } from "next/app";3import { LenisProvider } from "@/components/providers/lenis-provider";4import "../styles/globals.css";56export default function App({ Component, pageProps }: AppProps) {7 return (8 <LenisProvider>9 <Component {...pageProps} />10 </LenisProvider>11 );12}
Enhanced Configuration Options
Recent Lenis versions include new configuration options:
1import type { LenisOptions } from "lenis";23const lenisOptions: LenisOptions = {4 // Core options5 autoRaf: true, // New: automatic RAF handling6 duration: 1.2,7 easing: (t: number) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),89 // Direction and gesture10 direction: "vertical", // 'vertical', 'horizontal'11 gestureDirection: "vertical", // 'vertical', 'horizontal', 'both'1213 // Smoothing14 smooth: true,15 smoothTouch: false, // Recommended: false for mobile performance1617 // Multipliers18 mouseMultiplier: 1,19 touchMultiplier: 2,2021 // Advanced options22 infinite: false,23 syncTouch: false, // New: sync touch events24 syncTouchLerp: 0.075, // New: touch lerp value25 touchInertiaMultiplier: 35, // New: touch inertia2627 // Anchor links support28 anchors: true, // New: automatic anchor link handling2930 // Custom elements31 wrapper: typeof window !== "undefined" ? window : undefined, // Scroll container32 content:33 typeof document !== "undefined" ? document.documentElement : undefined, // Content element34 eventsTarget: typeof window !== "undefined" ? window : undefined, // Events target3536 // Overscroll behavior37 overscroll: true, // New: CSS overscroll-behavior support3839 // Modifiers40 modifierKey: false, // New: modifier key requirement4142 // Prevent smooth scrolling on specific elements43 prevent: (node: Element) => node.id === "modal", // Function to prevent scroll on specific elements44};4546const lenis = new Lenis(lenisOptions);
Preventing Scroll on Elements
Lenis provides multiple ways to prevent smooth scrolling on specific elements:
Method 1: Using the prevent function
1const lenis = new Lenis({2 prevent: (node: Element) => {3 // Prevent smooth scrolling on elements with specific IDs or classes4 return node.id === "modal" || node.classList.contains("no-smooth-scroll");5 },6});
Method 2: Using data attributes
Use these data attributes directly on HTML elements:
1// components/ScrollableSection.tsx2interface ScrollableSectionProps {3 children: React.ReactNode;4 height?: string;5}67export default function ScrollableSection({8 children,9 height = "200px",10}: ScrollableSectionProps) {11 return (12 <div>13 {/* Prevent all smooth scrolling */}14 <div data-lenis-prevent style={{ height, overflow: "auto" }}>15 {children}16 </div>1718 {/* Prevent only wheel events */}19 <div data-lenis-prevent-wheel style={{ height, overflow: "auto" }}>20 {children}21 </div>2223 {/* Prevent only touch events */}24 <div data-lenis-prevent-touch style={{ height, overflow: "auto" }}>25 {children}26 </div>27 </div>28 );29}
Scroll-to Functionality
Modern Scroll-to Implementation
Recent versions handle anchor links automatically when anchors: true
is set:
1// components/ScrollToButton.tsx2"use client";34import { useLenis } from "@/components/providers/lenis-provider";5import type { LenisOptions } from "lenis";67interface ScrollToButtonProps {8 target: string;9 children: React.ReactNode;10 options?: Partial<LenisOptions>;11}1213export default function ScrollToButton({14 target,15 children,16 options = {},17}: ScrollToButtonProps) {18 const lenis = useLenis();1920 const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {21 e.preventDefault();22 if (lenis) {23 lenis.scrollTo(target, {24 duration: 2,25 easing: (t: number) => 1 - Math.pow(1 - t, 3),26 ...options,27 });28 }29 };3031 return <button onClick={handleClick}>{children}</button>;32}
Anchor Links Configuration
For automatic anchor link handling with custom options:
1import type { LenisOptions } from "lenis";23const lenisConfig: LenisOptions = {4 anchors: {5 offset: 100, // Offset from target6 onComplete: () => {7 console.log("Scrolled to anchor");8 },9 },10};1112const lenis = new Lenis(lenisConfig);
Integration with GSAP (Updated)
The recommended GSAP ScrollTrigger integration has been updated for the latest versions:
1// components/ScrollAnimations.tsx2"use client";34import { useEffect } from "react";5import { gsap } from "gsap";6import { ScrollTrigger } from "gsap/ScrollTrigger";7import { useLenis } from "@/components/providers/lenis-provider";89gsap.registerPlugin(ScrollTrigger);1011interface ScrollAnimationsProps {12 children: React.ReactNode;13}1415export default function ScrollAnimations({ children }: ScrollAnimationsProps) {16 const lenis = useLenis();1718 useEffect(() => {19 if (lenis) {20 // Synchronize Lenis scrolling with GSAP's ScrollTrigger plugin21 lenis.on("scroll", ScrollTrigger.update);2223 // Add Lenis's requestAnimationFrame method to GSAP's ticker24 gsap.ticker.add((time: number) => {25 lenis.raf(time * 1000); // Convert time from seconds to milliseconds26 });2728 // Disable lag smoothing in GSAP to prevent any delay in scroll animations29 gsap.ticker.lagSmoothing(0);30 }3132 return () => {33 if (lenis) {34 lenis.off("scroll", ScrollTrigger.update);35 gsap.ticker.remove(lenis.raf);36 }37 };38 }, [lenis]);3940 return <>{children}</>;41}
Manual RAF Implementation (Alternative)
If you prefer manual control over the animation loop:
1// components/LenisProviderManual.tsx2"use client";34import { useEffect, useRef } from "react";5import Lenis from "lenis";6import type { LenisOptions } from "lenis";78interface LenisProviderManualProps {9 children: React.ReactNode;10 options?: LenisOptions;11}1213export default function LenisProviderManual({14 children,15 options = {},16}: LenisProviderManualProps) {17 const rafRef = useRef<number>();1819 useEffect(() => {20 const lenis = new Lenis({21 autoRaf: false, // Disable automatic RAF22 duration: 1.2,23 easing: (t: number) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),24 ...options,25 });2627 function raf(time: number) {28 lenis.raf(time);29 rafRef.current = requestAnimationFrame(raf);30 }3132 rafRef.current = requestAnimationFrame(raf);3334 return () => {35 if (rafRef.current) {36 cancelAnimationFrame(rafRef.current);37 }38 lenis.destroy();39 };40 }, [options]);4142 return <>{children}</>;43}
Performance Optimizations
Respect User Preferences
1import type { LenisOptions } from "lenis";23const getResponsiveLenisOptions = (): LenisOptions => {4 const prefersReducedMotion =5 typeof window !== "undefined"6 ? window.matchMedia("(prefers-reduced-motion: reduce)").matches7 : false;89 return {10 duration: prefersReducedMotion ? 0 : 1.2,11 smooth: !prefersReducedMotion,12 autoRaf: true,13 };14};1516const lenis = new Lenis(getResponsiveLenisOptions());
Conditional Loading for Mobile
1// components/ConditionalLenisProvider.tsx2"use client";34import { useEffect, useState } from "react";5import { LenisProvider } from "@/components/providers/lenis-provider";6import type { LenisOptions } from "lenis";78interface ConditionalLenisProviderProps {9 children: React.ReactNode;10 options?: LenisOptions;11}1213export default function ConditionalLenisProvider({14 children,15 options = {},16}: ConditionalLenisProviderProps) {17 const [shouldEnableLenis, setShouldEnableLenis] = useState<boolean>(false);1819 useEffect(() => {20 // Only initialize on devices that benefit from smooth scroll21 const shouldInit = window.innerWidth > 1024 && !("ontouchstart" in window);22 setShouldEnableLenis(shouldInit);23 }, []);2425 if (!shouldEnableLenis) {26 return <>{children}</>;27 }2829 return <LenisProvider options={options}>{children}</LenisProvider>;30}
Troubleshooting
Common Issues and Solutions
-
Smooth scrolling not working: Ensure
autoRaf: true
is set or implement manual RAF loop. -
Performance issues on mobile: Set
smoothTouch: false
for better mobile performance. -
Anchor links not working: Set
anchors: true
in Lenis configuration or implement custom click handlers. -
Next.js hydration errors: Always use
'use client'
directive and check forwindow
object availability. -
React 19 compatibility: Some third-party libraries may need updates for React 19 compatibility. Use the latest Lenis version which supports React 19.
-
Scroll conflicts in modals: Use
data-lenis-prevent
attribute or theprevent
function to disable smooth scrolling on specific elements.
Debugging Tips
1// Add debugging to your Lenis instance2import type { ScrollData } from "lenis";34const lenis = new Lenis({5 autoRaf: true,6 // ... other options7});89lenis.on("scroll", (data: ScrollData) => {10 console.log("Lenis scroll event:", data);11});
Complete Example (Next.js 15 + React 19)
1// app/layout.tsx2import { LenisProvider } from "@/components/providers/lenis-provider";3import "lenis/dist/lenis.css"; // Import Lenis CSS4import "./globals.css";56export default function RootLayout({7 children,8}: {9 children: React.ReactNode;10}) {11 return (12 <html lang="en">13 <body>14 <LenisProvider>{children}</LenisProvider>15 </body>16 </html>17 );18}
1// app/page.tsx2"use client";34import { useLenis } from "@/components/providers/lenis-provider";56const sections = [7 { id: "hero", title: "Hero", color: "lightblue" },8 { id: "about", title: "About", color: "lightgreen" },9 { id: "contact", title: "Contact", color: "lightcoral" },10] as const;1112export default function HomePage() {13 const lenis = useLenis();1415 const scrollToSection = (target: string) => {16 if (lenis) {17 lenis.scrollTo(target, { duration: 1.5 });18 }19 };2021 return (22 <div>23 <nav style={{ position: "fixed", top: 0, zIndex: 1000 }}>24 {sections.map((section) => (25 <button26 key={section.id}27 onClick={() => scrollToSection(`#${section.id}`)}28 >29 {section.title}30 </button>31 ))}32 </nav>3334 {sections.map((section) => (35 <section36 key={section.id}37 id={section.id}38 style={{ height: "100vh", background: section.color }}39 >40 <h1>{section.title} Section</h1>4142 {/* Example of preventing smooth scroll on specific content */}43 {section.id === "contact" && (44 <div45 data-lenis-prevent46 style={{47 height: "200px",48 overflow: "auto",49 background: "white",50 margin: "20px",51 }}52 >53 <p>This content uses native scrolling</p>54 <p>Scroll here won't be smooth</p>55 {/* Add more content to make it scrollable */}56 {Array.from({ length: 20 }, (_, i) => (57 <p key={i}>Line {i + 1}</p>58 ))}59 </div>60 )}61 </section>62 ))}63 </div>64 );65}
Migration from Old Versions
If upgrading from @studio-freight/lenis
:
- Update package: Replace
@studio-freight/lenis
withlenis
- Update imports: Change import paths to use the new package
- Use autoRaf: Take advantage of the new
autoRaf: true
option - Enable anchors: Set
anchors: true
for automatic anchor link handling - Update CSS: Ensure you have the latest required CSS classes or import via JS
- Add TypeScript types: Import and use proper types from the package
- Implement prevent logic: Use the new
prevent
function or data attributes for better control
Best Practices for 2025
- Use autoRaf: Take advantage of the new
autoRaf
option for simpler setup - Enable anchor support: Set
anchors: true
for better UX - Respect user preferences: Check for
prefers-reduced-motion
- Optimize for mobile: Use
smoothTouch: false
for better performance - Leverage React 19 features: Take advantage of React 19's improvements in Next.js 15.1+
- Use TypeScript: Add proper type definitions for better DX
- Implement proper error boundaries: Handle cases where Lenis fails to initialize
- Use prevent mechanisms: Implement proper scroll prevention for modals and nested scrollable areas
- Import CSS efficiently: Use JavaScript imports for better bundling when possible
This implementation provides smooth, performant scrolling throughout your Next.js 15 application while maintaining compatibility with React 19 and following current best practices with full TypeScript support.