# Lenis React Integration Complete Lenis smooth scroll integration for React/Next.js with GSAP ScrollTrigger. ## Table of Contents 1. [Installation](#installation) 2. [Basic Setup](#basic-setup) 3. [GSAP Integration](#gsap-integration) 4. [useLenis Hook](#uselenis-hook) 5. [Configuration Options](#configuration-options) 6. [Common Patterns](#common-patterns) 7. [Troubleshooting](#troubleshooting) ## Installation ```bash npm install lenis # Package includes React components at lenis/react ``` ## Basic Setup ### Provider Component ```tsx // components/SmoothScroll.tsx 'use client' import { ReactLenis } from 'lenis/react' export function SmoothScroll({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` ### Layout Integration ```tsx // app/layout.tsx import { SmoothScroll } from '@/components/SmoothScroll' export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` ### Required CSS ```css /* globals.css */ 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; } ``` ## GSAP Integration ### Full Integration (Recommended) ```tsx // components/SmoothScroll.tsx 'use client' import { ReactLenis, useLenis } from 'lenis/react' import { useEffect, useRef } from 'react' import gsap from 'gsap' import { ScrollTrigger } from 'gsap/ScrollTrigger' gsap.registerPlugin(ScrollTrigger) function LenisGSAPConnector() { const lenis = useLenis() useEffect(() => { if (!lenis) return // Connect Lenis scroll to ScrollTrigger lenis.on('scroll', ScrollTrigger.update) // Use GSAP ticker for RAF const update = (time: number) => { lenis.raf(time * 1000) } gsap.ticker.add(update) gsap.ticker.lagSmoothing(0) return () => { gsap.ticker.remove(update) lenis.off('scroll', ScrollTrigger.update) } }, [lenis]) return null } export function SmoothScroll({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` ### Alternative: Ref-based Integration ```tsx 'use client' import { ReactLenis, type LenisRef } from 'lenis/react' import { useEffect, useRef } from 'react' import gsap from 'gsap' import { ScrollTrigger } from 'gsap/ScrollTrigger' gsap.registerPlugin(ScrollTrigger) export function SmoothScroll({ children }: { children: React.ReactNode }) { const lenisRef = useRef(null) useEffect(() => { const lenis = lenisRef.current?.lenis if (!lenis) return lenis.on('scroll', ScrollTrigger.update) const update = (time: number) => { lenis.raf(time * 1000) } gsap.ticker.add(update) gsap.ticker.lagSmoothing(0) return () => { gsap.ticker.remove(update) } }, []) return ( {children} ) } ``` ## useLenis Hook ### Basic Usage ```tsx 'use client' import { useLenis } from 'lenis/react' function ScrollProgress() { const [progress, setProgress] = useState(0) useLenis(({ scroll, limit, velocity, direction }) => { setProgress(scroll / limit) console.log({ scroll, limit, velocity, direction }) }) return (
) } ``` ### Scroll to Element ```tsx 'use client' import { useLenis } from 'lenis/react' function ScrollToSection() { const lenis = useLenis() const scrollToSection = (id: string) => { const element = document.getElementById(id) if (element && lenis) { lenis.scrollTo(element, { offset: -100, // Offset from top duration: 1.5, // Animation duration easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), }) } } return ( ) } ``` ### Stop/Start Scroll ```tsx 'use client' import { useLenis } from 'lenis/react' function Modal({ isOpen, onClose, children }) { const lenis = useLenis() useEffect(() => { if (isOpen) { lenis?.stop() } else { lenis?.start() } }, [isOpen, lenis]) if (!isOpen) return null return (
{children}
) } ``` ### Velocity-Based Effects ```tsx 'use client' import { useLenis } from 'lenis/react' import { useRef } from 'react' import gsap from 'gsap' function VelocitySkew() { const textRef = useRef(null) useLenis(({ velocity }) => { gsap.to(textRef.current, { skewY: velocity * 0.05, duration: 0.3, ease: 'power2.out', }) }) return

Velocity Skew Text

} ``` ## Configuration Options ### Full Options Reference ```tsx {children} ``` ### Common Presets ```tsx // Premium feel (Studio Freight style) const premiumOptions = { lerp: 0.075, duration: 1.5, smoothWheel: true, wheelMultiplier: 0.8, } // Snappy feel const snappyOptions = { lerp: 0.15, duration: 0.8, smoothWheel: true, wheelMultiplier: 1.2, } // Mobile-friendly const mobileOptions = { lerp: 0.1, duration: 1.2, touchMultiplier: 1.5, syncTouch: true, syncTouchLerp: 0.075, } ``` ## Common Patterns ### Prevent Scroll on Specific Elements ```tsx // Add data-lenis-prevent to prevent smooth scroll
// Or data-lenis-prevent-wheel for wheel only
...
``` ### Anchor Links ```tsx 'use client' import { useLenis } from 'lenis/react' import { useEffect } from 'react' function AnchorHandler() { const lenis = useLenis() useEffect(() => { // Handle anchor links const handleClick = (e: MouseEvent) => { const target = e.target as HTMLAnchorElement if (target.hash) { e.preventDefault() const element = document.querySelector(target.hash) if (element) { lenis?.scrollTo(element as HTMLElement, { offset: -100 }) } } } document.addEventListener('click', handleClick) return () => document.removeEventListener('click', handleClick) }, [lenis]) return null } ``` ### Scroll Direction Detection ```tsx 'use client' import { useLenis } from 'lenis/react' import { useState } from 'react' function DirectionAwareHeader() { const [isVisible, setIsVisible] = useState(true) useLenis(({ direction }) => { setIsVisible(direction <= 0) // Show on scroll up }) return (
Header
) } ``` ### Horizontal Scroll Section with Lenis ```tsx 'use client' import { useLenis } from 'lenis/react' import { useRef, useEffect } from 'react' import gsap from 'gsap' import { ScrollTrigger } from 'gsap/ScrollTrigger' import { useGSAP } from '@gsap/react' gsap.registerPlugin(ScrollTrigger) function HorizontalSection() { const containerRef = useRef(null) const wrapperRef = useRef(null) useGSAP(() => { const sections = gsap.utils.toArray('.h-section') gsap.to(sections, { xPercent: -100 * (sections.length - 1), ease: 'none', scrollTrigger: { trigger: wrapperRef.current, pin: true, scrub: 1, snap: 1 / (sections.length - 1), end: () => '+=' + wrapperRef.current!.scrollWidth, } }) }, { scope: containerRef }) return (
{[1, 2, 3, 4].map(i => (
Section {i}
))}
) } ``` ## Troubleshooting ### ScrollTrigger Not Working ```tsx // Always refresh after Lenis initializes useEffect(() => { if (lenis) { ScrollTrigger.refresh() } }, [lenis]) ``` ### Jerky Scroll on Mobile ```tsx // Enable syncTouch for better mobile performance ``` ### Scroll Not Working in Modals ```tsx // Stop Lenis when modal opens useEffect(() => { if (isModalOpen) { lenis?.stop() } else { lenis?.start() } }, [isModalOpen, lenis]) ``` ### Performance Issues 1. **Disable on low-power mode**: Check `navigator.getBattery()` for charging state 2. **Reduce lerp value**: Higher lerp = less calculations 3. **Use `autoRaf: true`** if not using GSAP ticker ### Next.js App Router Specifics 1. Always use `'use client'` directive 2. Wrap in layout.tsx for global smooth scroll 3. Use `usePathname()` to reset scroll on navigation: ```tsx 'use client' import { useLenis } from 'lenis/react' import { usePathname } from 'next/navigation' import { useEffect } from 'react' function ScrollReset() { const lenis = useLenis() const pathname = usePathname() useEffect(() => { lenis?.scrollTo(0, { immediate: true }) }, [pathname, lenis]) return null } ```