Logo

Bridger Tower / Designer and Software Engineer

How to implement Lenis in Next.js

This guide covers implementing Lenis in Next.js 15 with React 19 support and the latest best practices.

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:

bash
1npm install lenis
2# or
3yarn add lenis
4# or
5pnpm 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:

tsx
1// components/providers/lenis-provider.tsx
2"use client";
3
4import { createContext, useContext, useEffect, useRef } from "react";
5import Lenis from "lenis";
6
7import type { LenisOptions } from "lenis";
8
9const LenisContext = createContext<Lenis | null>(null);
10
11export function LenisProvider({
12 children,
13 options = {},
14}: {
15 children: React.ReactNode;
16 options?: LenisOptions;
17}) {
18 const lenisRef = useRef<Lenis | null>(null);
19
20 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 });
30
31 lenisRef.current = lenis;
32
33 return () => {
34 lenis.destroy();
35 lenisRef.current = null;
36 };
37 }, [options]);
38
39 return (
40 <LenisContext.Provider value={lenisRef.current}>
41 {children}
42 </LenisContext.Provider>
43 );
44}
45
46export 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):

tsx
1// Import in your main component or layout
2import "lenis/dist/lenis.css";

Option 2: Add to your global CSS:

css
1/* Add to your global CSS */
2html.lenis,
3html.lenis body {
4 height: auto;
5}
6
7.lenis.lenis-smooth {
8 scroll-behavior: auto !important;
9}
10
11.lenis.lenis-smooth [data-lenis-prevent] {
12 overscroll-behavior: contain;
13}
14
15.lenis.lenis-stopped {
16 overflow: hidden;
17}
18
19.lenis.lenis-smooth iframe {
20 pointer-events: none;
21}

Step 3: Integration with Next.js 15

For App Router (Next.js 13+):

tsx
1// app/layout.tsx
2import { LenisProvider } from "@/components/providers/lenis-provider";
3import "./globals.css";
4
5export 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):

tsx
1// pages/_app.tsx
2import type { AppProps } from "next/app";
3import { LenisProvider } from "@/components/providers/lenis-provider";
4import "../styles/globals.css";
5
6export 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:

tsx
1import type { LenisOptions } from "lenis";
2
3const lenisOptions: LenisOptions = {
4 // Core options
5 autoRaf: true, // New: automatic RAF handling
6 duration: 1.2,
7 easing: (t: number) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
8
9 // Direction and gesture
10 direction: "vertical", // 'vertical', 'horizontal'
11 gestureDirection: "vertical", // 'vertical', 'horizontal', 'both'
12
13 // Smoothing
14 smooth: true,
15 smoothTouch: false, // Recommended: false for mobile performance
16
17 // Multipliers
18 mouseMultiplier: 1,
19 touchMultiplier: 2,
20
21 // Advanced options
22 infinite: false,
23 syncTouch: false, // New: sync touch events
24 syncTouchLerp: 0.075, // New: touch lerp value
25 touchInertiaMultiplier: 35, // New: touch inertia
26
27 // Anchor links support
28 anchors: true, // New: automatic anchor link handling
29
30 // Custom elements
31 wrapper: typeof window !== "undefined" ? window : undefined, // Scroll container
32 content:
33 typeof document !== "undefined" ? document.documentElement : undefined, // Content element
34 eventsTarget: typeof window !== "undefined" ? window : undefined, // Events target
35
36 // Overscroll behavior
37 overscroll: true, // New: CSS overscroll-behavior support
38
39 // Modifiers
40 modifierKey: false, // New: modifier key requirement
41
42 // Prevent smooth scrolling on specific elements
43 prevent: (node: Element) => node.id === "modal", // Function to prevent scroll on specific elements
44};
45
46const 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

tsx
1const lenis = new Lenis({
2 prevent: (node: Element) => {
3 // Prevent smooth scrolling on elements with specific IDs or classes
4 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:

tsx
1// components/ScrollableSection.tsx
2interface ScrollableSectionProps {
3 children: React.ReactNode;
4 height?: string;
5}
6
7export 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>
17
18 {/* Prevent only wheel events */}
19 <div data-lenis-prevent-wheel style={{ height, overflow: "auto" }}>
20 {children}
21 </div>
22
23 {/* 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:

tsx
1// components/ScrollToButton.tsx
2"use client";
3
4import { useLenis } from "@/components/providers/lenis-provider";
5import type { LenisOptions } from "lenis";
6
7interface ScrollToButtonProps {
8 target: string;
9 children: React.ReactNode;
10 options?: Partial<LenisOptions>;
11}
12
13export default function ScrollToButton({
14 target,
15 children,
16 options = {},
17}: ScrollToButtonProps) {
18 const lenis = useLenis();
19
20 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 };
30
31 return <button onClick={handleClick}>{children}</button>;
32}

Anchor Links Configuration

For automatic anchor link handling with custom options:

tsx
1import type { LenisOptions } from "lenis";
2
3const lenisConfig: LenisOptions = {
4 anchors: {
5 offset: 100, // Offset from target
6 onComplete: () => {
7 console.log("Scrolled to anchor");
8 },
9 },
10};
11
12const lenis = new Lenis(lenisConfig);

Integration with GSAP (Updated)

The recommended GSAP ScrollTrigger integration has been updated for the latest versions:

tsx
1// components/ScrollAnimations.tsx
2"use client";
3
4import { useEffect } from "react";
5import { gsap } from "gsap";
6import { ScrollTrigger } from "gsap/ScrollTrigger";
7import { useLenis } from "@/components/providers/lenis-provider";
8
9gsap.registerPlugin(ScrollTrigger);
10
11interface ScrollAnimationsProps {
12 children: React.ReactNode;
13}
14
15export default function ScrollAnimations({ children }: ScrollAnimationsProps) {
16 const lenis = useLenis();
17
18 useEffect(() => {
19 if (lenis) {
20 // Synchronize Lenis scrolling with GSAP's ScrollTrigger plugin
21 lenis.on("scroll", ScrollTrigger.update);
22
23 // Add Lenis's requestAnimationFrame method to GSAP's ticker
24 gsap.ticker.add((time: number) => {
25 lenis.raf(time * 1000); // Convert time from seconds to milliseconds
26 });
27
28 // Disable lag smoothing in GSAP to prevent any delay in scroll animations
29 gsap.ticker.lagSmoothing(0);
30 }
31
32 return () => {
33 if (lenis) {
34 lenis.off("scroll", ScrollTrigger.update);
35 gsap.ticker.remove(lenis.raf);
36 }
37 };
38 }, [lenis]);
39
40 return <>{children}</>;
41}

Manual RAF Implementation (Alternative)

If you prefer manual control over the animation loop:

tsx
1// components/LenisProviderManual.tsx
2"use client";
3
4import { useEffect, useRef } from "react";
5import Lenis from "lenis";
6import type { LenisOptions } from "lenis";
7
8interface LenisProviderManualProps {
9 children: React.ReactNode;
10 options?: LenisOptions;
11}
12
13export default function LenisProviderManual({
14 children,
15 options = {},
16}: LenisProviderManualProps) {
17 const rafRef = useRef<number>();
18
19 useEffect(() => {
20 const lenis = new Lenis({
21 autoRaf: false, // Disable automatic RAF
22 duration: 1.2,
23 easing: (t: number) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
24 ...options,
25 });
26
27 function raf(time: number) {
28 lenis.raf(time);
29 rafRef.current = requestAnimationFrame(raf);
30 }
31
32 rafRef.current = requestAnimationFrame(raf);
33
34 return () => {
35 if (rafRef.current) {
36 cancelAnimationFrame(rafRef.current);
37 }
38 lenis.destroy();
39 };
40 }, [options]);
41
42 return <>{children}</>;
43}

Performance Optimizations

Respect User Preferences

tsx
1import type { LenisOptions } from "lenis";
2
3const getResponsiveLenisOptions = (): LenisOptions => {
4 const prefersReducedMotion =
5 typeof window !== "undefined"
6 ? window.matchMedia("(prefers-reduced-motion: reduce)").matches
7 : false;
8
9 return {
10 duration: prefersReducedMotion ? 0 : 1.2,
11 smooth: !prefersReducedMotion,
12 autoRaf: true,
13 };
14};
15
16const lenis = new Lenis(getResponsiveLenisOptions());

Conditional Loading for Mobile

tsx
1// components/ConditionalLenisProvider.tsx
2"use client";
3
4import { useEffect, useState } from "react";
5import { LenisProvider } from "@/components/providers/lenis-provider";
6import type { LenisOptions } from "lenis";
7
8interface ConditionalLenisProviderProps {
9 children: React.ReactNode;
10 options?: LenisOptions;
11}
12
13export default function ConditionalLenisProvider({
14 children,
15 options = {},
16}: ConditionalLenisProviderProps) {
17 const [shouldEnableLenis, setShouldEnableLenis] = useState<boolean>(false);
18
19 useEffect(() => {
20 // Only initialize on devices that benefit from smooth scroll
21 const shouldInit = window.innerWidth > 1024 && !("ontouchstart" in window);
22 setShouldEnableLenis(shouldInit);
23 }, []);
24
25 if (!shouldEnableLenis) {
26 return <>{children}</>;
27 }
28
29 return <LenisProvider options={options}>{children}</LenisProvider>;
30}

Troubleshooting

Common Issues and Solutions

  1. Smooth scrolling not working: Ensure autoRaf: true is set or implement manual RAF loop.

  2. Performance issues on mobile: Set smoothTouch: false for better mobile performance.

  3. Anchor links not working: Set anchors: true in Lenis configuration or implement custom click handlers.

  4. Next.js hydration errors: Always use 'use client' directive and check for window object availability.

  5. React 19 compatibility: Some third-party libraries may need updates for React 19 compatibility. Use the latest Lenis version which supports React 19.

  6. Scroll conflicts in modals: Use data-lenis-prevent attribute or the prevent function to disable smooth scrolling on specific elements.

Debugging Tips

tsx
1// Add debugging to your Lenis instance
2import type { ScrollData } from "lenis";
3
4const lenis = new Lenis({
5 autoRaf: true,
6 // ... other options
7});
8
9lenis.on("scroll", (data: ScrollData) => {
10 console.log("Lenis scroll event:", data);
11});

Complete Example (Next.js 15 + React 19)

tsx
1// app/layout.tsx
2import { LenisProvider } from "@/components/providers/lenis-provider";
3import "lenis/dist/lenis.css"; // Import Lenis CSS
4import "./globals.css";
5
6export 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}
tsx
1// app/page.tsx
2"use client";
3
4import { useLenis } from "@/components/providers/lenis-provider";
5
6const 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;
11
12export default function HomePage() {
13 const lenis = useLenis();
14
15 const scrollToSection = (target: string) => {
16 if (lenis) {
17 lenis.scrollTo(target, { duration: 1.5 });
18 }
19 };
20
21 return (
22 <div>
23 <nav style={{ position: "fixed", top: 0, zIndex: 1000 }}>
24 {sections.map((section) => (
25 <button
26 key={section.id}
27 onClick={() => scrollToSection(`#${section.id}`)}
28 >
29 {section.title}
30 </button>
31 ))}
32 </nav>
33
34 {sections.map((section) => (
35 <section
36 key={section.id}
37 id={section.id}
38 style={{ height: "100vh", background: section.color }}
39 >
40 <h1>{section.title} Section</h1>
41
42 {/* Example of preventing smooth scroll on specific content */}
43 {section.id === "contact" && (
44 <div
45 data-lenis-prevent
46 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:

  1. Update package: Replace @studio-freight/lenis with lenis
  2. Update imports: Change import paths to use the new package
  3. Use autoRaf: Take advantage of the new autoRaf: true option
  4. Enable anchors: Set anchors: true for automatic anchor link handling
  5. Update CSS: Ensure you have the latest required CSS classes or import via JS
  6. Add TypeScript types: Import and use proper types from the package
  7. Implement prevent logic: Use the new prevent function or data attributes for better control

Best Practices for 2025

  1. Use autoRaf: Take advantage of the new autoRaf option for simpler setup
  2. Enable anchor support: Set anchors: true for better UX
  3. Respect user preferences: Check for prefers-reduced-motion
  4. Optimize for mobile: Use smoothTouch: false for better performance
  5. Leverage React 19 features: Take advantage of React 19's improvements in Next.js 15.1+
  6. Use TypeScript: Add proper type definitions for better DX
  7. Implement proper error boundaries: Handle cases where Lenis fails to initialize
  8. Use prevent mechanisms: Implement proper scroll prevention for modals and nested scrollable areas
  9. 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.

© Bridger Tower, 2025