Smooth Scrolling: Far beyond mere eye candy, it's about elevating how users navigate your site. This delivers deliberate, fluid movement between page sections instead of the harsh snaps of standard scrolling.
Beyond looks alone, it crafts a storytelling rhythm that leads visitors through your material naturally and immersively.
Parallax Effect: Picture content layers shifting at varied paces during scroll, forging a sense of depth and engagement. That's parallax in action.
It infuses 3D-like dimension into flat web layouts, boosting interactivity and allure. Applied thoughtfully, it transforms basic scrolling into a narrative adventure that captivates and holds attention. In Next.js, we harness Lenis and GSAP to make this real.
Lenis offers a lightweight solution for seamless scrolling with straightforward implementation.
GSAP delivers powerhouse performance for animations like parallax. Combined, they create a dynamic powerhouse for Next.js builds. Time to dive in.
Before coding begins, establish your Next.js foundation. Confirm Node.js is ready on your machine. Launch terminal and execute:
npx create-next-app@latestIt'll prompt several choices—pick these:
√ What is your project named? ... nextjs-smooth-scroll
√ Would you like to use TypeScript? ... No
√ Would you like to use ESLint? ... Yes
√ Would you like to use Tailwind CSS? ... Yes
√ Would you like to use src/ directory? ... Yes
√ Would you like to use App Router? (recommended) ... Yes
? Would you like to customize the default import alias (@/*)? » NoAfter dependencies finish, enter your project folder with cd nextjs-smooth-scroll and grab the scroll + animation packages:
npm install gsap @studio-freight/react-lenisLaunch with npm run dev—check it at http://localhost:3000.
Inside your Next.js app, craft SmoothScrolling.jsx within the components directory. This leverages @studio-freight/react-lenis, the React-friendly Lenis wrapper. Dive deeper via its documentation.
"use client";
import { ReactLenis } from "@studio-freight/react-lenis";
function SmoothScrolling({ children }) {
return (
<ReactLenis root options={{ lerp: 0.1, duration: 1.5, smoothTouch: true }}>
{children}
</ReactLenis>
);
}
export default SmoothScrolling;Line 1: 'use client' ensures browser-side execution, skipping server rendering.
Line 4: SmoothScrolling accepts children props, letting wrapped elements gain smooth scroll inheritance.
Line 6: Delivers ReactLenis with tailored settings.
root signals full-page scroll control via <html> element.
options shapes the scroll feel:
lerp: 0.1—Sets interpolation for silky motion (smaller = silkier).
duration: 1.5—Defines seconds per scroll gesture (higher = gentler pace).
smoothTouch: true—Activates fluid touch on mobiles. Explore more in its documentation.
Now import SmoothScrolling.jsx into layout.js and enclose {children}:
import { Inter } from "next/font/google";
import "./globals.css";
import SmoothScrolling from "@/components/SmoothScrolling";
const inter = Inter({ subsets: ["latin"] });
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
<SmoothScrolling>{children}</SmoothScrolling>
</body>
</html>
);
}With SmoothScrolling embedded, your Next.js app gains premium scroll polish. Test it by building an ImageList.jsx to showcase images.
In components, add ImageList.jsx using picsum.photos for visuals:
import React from "react";
import Image from "next/image";
const ImageList = () => {
return (
<>
<Image
src={"https://picsum.photos/600/400?random=1"}
alt="Image"
width={600}
height={400}
priority
sizes="50vw"
/>
<Image
src={"https://picsum.photos/600/400?random=2"}
alt="Image"
width={600}
height={400}
priority
sizes="50vw"
/>
<Image
src={"https://picsum.photos/400/600?random=3"}
alt="Image"
width={400}
height={600}
sizes="50vw"
/>
<Image
src={"https://picsum.photos/600/400?random=4"}
alt="Image"
width={600}
height={400}
sizes="50vw"
/>
<Image
src={"https://picsum.photos/600/400?random=5"}
alt="Image"
width={600}
height={400}
sizes="50vw"
/>
{/* Stack more if desired */}
</>
);
};
export default ImageList;These leverage picsum.photos URLs—tweak width/height params for variety. Next.js Image handles optimization. First, whitelist picsum in next.config.js:
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "picsum.photos",
},
],
},
};
module.exports = nextConfig;Render ImageList.jsx in page.js—clear existing content:
import ImageList from "@/components/ImageList";
export default function Home() {
return (
<main className="p-16 xl:p-32 flex flex-col w-full items-center justify-center">
<ImageList />
</main>
);
}Done. Feel the buttery scroll in action.
Next, inject parallax via GSAP (GreenSock Animation Platform). Build Parallax.jsx in components. We'll dissect it, then integrate into ImageList.jsx.
Start with imports:
"use client";
import { gsap } from "gsap";
import { useEffect, useRef } from "react";
import { useWindowSize } from "@studio-freight/hamo";
import { ScrollTrigger } from "gsap/ScrollTrigger";'use client' guarantees client execution. GSAP's ScrollTrigger plugin precisely handles image positioning.useWindowSize from @studio-freight/hamo grabs viewport width—install via:
npm i @studio-freight/hamoCore structure:
export function Parallax({ className, children, speed = 1, id = "parallax" }) {
const trigger = useRef();
const target = useRef();
const timeline = useRef();
const { width: windowWidth } = useWindowSize();
return (
<div ref={trigger} className={className}>
<div ref={target}>{children}</div>
</div>
);
}Line 1: Defines Parallax with props: className (styling), children (content), speed (motion rate), id (ScrollTrigger key).
Lines 2-4: useRef creates: trigger (animation starter), target (animated element), timeline (sequence controller).
Line 5: Extracts windowWidth via useWindowSize().
Lines 8-9: Renders trigger wrapping animated target holding children.
Add useEffect for animation:
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
const y = windowWidth * speed * 0.1;
const setY = gsap.quickSetter(target.current, "y", "px");
timeline.current = gsap.timeline({
scrollTrigger: {
id: id,
trigger: trigger.current,
scrub: true,
start: "top bottom",
end: "bottom top",
onUpdate: (e) => {
setY(e.progress * y);
},
},
});
return () => {
timeline?.current?.kill();
};
}, [id, speed, windowWidth]);Line 2: Activates ScrollTrigger for scroll-tied animations.
Line 4: Computes y travel based on viewport and speed.
Line 5: gsap.quickSetter() builds efficient y-position updater for target in pixels.
Lines 7-18: Builds timeline with ScrollTrigger:
id: Unique instance tag.
trigger: Animation kickoff element.
scrub: true: Locks animation to scroll (no jumps).
start/end: Pinpoints animation window ("top bottom" = trigger-top hits viewport-bottom).
onUpdate: Shifts y by scroll progress.
Line 21: Cleanup kills instances on unmount/dependency shifts, dodging leaks.
Wire Parallax.jsx into ImageList.jsx for image depth:
import React from "react";
import { Parallax } from "@/components/Parallax";
import Image from "next/image";
const ImageList = () => {
return (
<>
<Parallax speed={1} className="self-start">
<Image
src={"https://picsum.photos/600/400?random=1"}
alt="Image"
width={600}
height={400}
priority
sizes="50vw"
/>
</Parallax>
<Parallax speed={-2} className="self-end overflow-hidden">
<Image
src={"https://picsum.photos/600/400?random=2"}
alt="Image"
width={600}
height={400}
priority
sizes="50vw"
/>
</Parallax>
{/* Continue pattern... */}
</>
);
};
export default ImageList;
Import Parallax from components, wrap each Image. Vary speed values to watch motion differ. Add "overflow-hidden" to className and observe dynamic height shifts on scroll.
Performance Watch: Test on mobile/low-end devices. Lower lerp = smoother but heavier CPU.
UX Balance: Subtle effects enhance—overkill confuses.
Asset Tuning: Compress images aggressively.
Animation Restraint: Few targeted effects > many competing ones.
Responsive Handling: Scale speed by screen size.
Accessibility First: Respect prefers-reduced-motion.
This guide covered weaving smooth scroll and parallax into Next.js via Lenis + GSAP. These unleash stunning interactivity. Success hinges on restraint and optimization.