How to Detect and Handle Smooth Scroll Libraries Like Lenis or Locomotive Scroll in Next.js

Detect smooth scroll libraries by scanning for CSS classes (.lenis, .locomotive-scroll), script sources, and initialization patterns, then implement them in your Next.js clone using client-side React hooks with proper cleanup to prevent memory leaks.

When reverse-engineering websites with the JCodesMore/ai-website-cloner-template, accurately reproducing the scroll experience requires identifying and implementing smooth scroll libraries like Lenis or Locomotive Scroll. These libraries hijack native scrolling to create inertia-based animations, but they leave distinct fingerprints in the DOM and network requests that you can detect programmatically. This guide covers the detection logic defined in the template's inspection workflows and the exact implementation steps for Next.js App Router projects.

Detecting Smooth Scroll Libraries

The template's inspection process, documented in docs/research/INSPECTION_GUIDE.md and the clone workflows, identifies smooth scroll libraries through specific DOM signatures and script patterns.

Lenis Detection Signatures

Lenis typically reveals itself through:

  • CSS class .lenis on wrapper elements that enable the smooth-scroll engine
  • Script tags importing lenis.js or @lenis/core from CDNs or npm
  • JavaScript initialization patterns like new Lenis(...) or lenis.raf(time)
  • Global stylesheet modifications that hide native scrollbars via overflow: hidden on the html element

According to the template's .windsurf/workflows/clone-website.md, these markers are checked during the visual inspection phase.

Locomotive Scroll Detection Patterns

Locomotive Scroll uses different conventions:

  • Container class .locomotive-scroll on the primary scroll wrapper
  • Data attributes like data-scroll-container on the main content wrapper
  • Script sources containing locomotive-scroll.min.js or the npm package locomotive-scroll
  • Constructor calls instantiating new LocomotiveScroll({ el: ..., smooth: true })

The .opencode/commands/clone-website.md file explicitly lists these signatures as critical detection points for the cloning workflow.

Automated Detection with Playwright

For automated inspection, use a headless browser script to scan for these patterns:

// utils/detect-scroll.ts
import { Page } from 'playwright';

export async function detectSmoothScroll(page: Page) {
  const hasLenis = await page.$('.lenis');
  const hasLocomotive = await page.$('.locomotive-scroll');
  const scripts = await page.$$eval('script[src]', (els) => 
    els.map((e) => e.getAttribute('src'))
  );
  const usesLenis = scripts.some((src) => src?.includes('lenis'));
  const usesLocomotive = scripts.some((src) => src?.includes('locomotive-scroll'));

  return {
    lenis: !!hasLenis || usesLenis,
    locomotive: !!hasLocomotive || usesLocomotive,
  };
}

This detection logic mirrors the checklist in the template's inspection guide, ensuring you flag the correct library before generating code.

Implementing Smooth Scroll in Next.js

Once detected, implementing these libraries in a Next.js App Router project requires client-side initialization to avoid server-side rendering mismatches.

Installing Dependencies

Add the detected library to your package.json:

{
  "dependencies": {
    "lenis": "^1.0.0",
    "locomotive-scroll": "^4.1.4"
  }
}

The template's clone command automatically injects these dependencies when the detection phase identifies a smooth scroll library.

Lenis Implementation

Create a client-side wrapper in src/app/layout.tsx with the .lenis class:

'use client';
import { useEffect } from 'react';
import Lenis from '@lenis/core';
import '@/app/globals.css';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    const lenis = new Lenis({
      duration: 1.2,
      easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
    });

    function raf(time: number) {
      lenis.raf(time);
      requestAnimationFrame(raf);
    }
    requestAnimationFrame(raf);

    return () => lenis.destroy();
  }, []);

  return (
    <html lang="en">
      <body className="lenis">{children}</body>
    </html>
  );
}

The lenis.raf() method drives the animation frame updates, while lenis.destroy() in the cleanup function prevents memory leaks during route transitions.

Locomotive Scroll Setup

For Locomotive Scroll, import the required CSS and initialize with the container element:

'use client';
import { useEffect } from 'react';
import LocomotiveScroll from 'locomotive-scroll';
import 'locomotive-scroll/dist/locomotive-scroll.css';
import '@/app/globals.css';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    const scroll = new LocomotiveScroll({
      el: document.querySelector('.locomotive-scroll') as HTMLElement,
      smooth: true,
    });

    return () => scroll.destroy();
  }, []);

  return (
    <html lang="en">
      <body className="locomotive-scroll">{children}</body>
    </html>
  );
}

The el parameter must reference an element with the .locomotive-scroll class, matching the detection pattern used during inspection.

Cleanup and Performance

Both libraries attach global event listeners and animation frames. Always implement cleanup routines:

  • Lenis: Call lenis.destroy() to remove event listeners and stop the raf loop
  • Locomotive Scroll: Call scroll.destroy() to unbind scroll handlers and remove injected styles

Without cleanup, navigating between pages in your Next.js app creates duplicate scroll instances, causing jitter and memory leaks.

Configuration Files and Global Styles

Update src/app/globals.css to match the original site's scroll behavior:

/* Hide native scrollbars when using smooth scroll libraries */
html {
  overflow: hidden;
  height: 100%;
}

body {
  height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
}

The template references src/app/globals.css as the location for global scroll-related styles, ensuring visual consistency with the source website.

Summary

  • Detection relies on CSS classes (.lenis, .locomotive-scroll), script sources, and initialization patterns found in the template's inspection workflows
  • Implementation requires wrapper elements with specific classes and client-side initialization using 'use client' directives in Next.js
  • Cleanup is mandatory—both libraries require destroy() methods to prevent memory leaks during route transitions
  • Global styles in src/app/globals.css must account for hidden scrollbars and container heights to match the original experience
  • Reference files include .windsurf/workflows/clone-website.md, .opencode/commands/clone-website.md, and docs/research/INSPECTION_GUIDE.md for the complete detection logic

Frequently Asked Questions

How do I programmatically detect if a website uses Lenis or Locomotive Scroll?

Automate detection by scanning for CSS classes (.lenis or .locomotive-scroll), checking script src attributes for filenames containing "lenis" or "locomotive-scroll", and searching the JavaScript bundle for constructor patterns like new Lenis() or new LocomotiveScroll(). The template's Playwright-based detection script in utils/detect-scroll.ts combines these checks into a single boolean return for each library.

Why does my Locomotive Scroll implementation break in Next.js?

Locomotive Scroll requires a DOM element reference for its el parameter, which doesn't exist during server-side rendering. You must mark the component as 'use client' and initialize the library inside a useEffect hook. Additionally, failing to import the library's CSS (locomotive-scroll/dist/locomotive-scroll.css) or missing the .locomotive-scroll class on your container element will cause the scroll calculations to fail.

What's the difference between Lenis and Locomotive Scroll detection?

Lenis typically uses the .lenis class on the body or a wrapper element and relies on requestAnimationFrame loops via lenis.raf(). Locomotive Scroll uses .locomotive-scroll classes combined with data-scroll-container attributes and requires a specific DOM element reference during initialization. The template's inspection guide in docs/research/INSPECTION_GUIDE.md treats these as separate detection paths because their implementation code differs significantly.

Where should I initialize smooth scroll libraries in a Next.js App Router application?

Initialize smooth scroll libraries in src/app/layout.tsx wrapped in a useEffect hook with a 'use client' directive at the top of the file. This ensures the code runs only in the browser after hydration. Place the initialization logic in the root layout to maintain smooth scrolling across all routes, and always return a cleanup function that calls the library's destroy() method to handle route transitions properly.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →