feat(smooth-scroll): implement smooth scrolling functionality with Lenis integration

This commit is contained in:
2026-03-12 19:08:44 +01:00
parent 8f6a242273
commit ef9b277396
6 changed files with 160 additions and 1 deletions

View File

@@ -62,6 +62,23 @@ body::before {
will-change: transform;
}
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;
}
.font-heading {
font-family: "Space Grotesk", "Manrope", "Segoe UI", sans-serif;
}

View File

@@ -3,6 +3,8 @@ import type { Metadata } from "next";
import "./globals.css";
import "currency-flags/dist/currency-flags.min.css";
import { SmoothScrollProvider } from "@/components/providers/smooth-scroll-provider";
export const metadata: Metadata = {
title: "NexCurrency | Modern Currency & Crypto Converter",
description:
@@ -17,7 +19,7 @@ export default function RootLayout({
return (
<html lang="en" className="dark" suppressHydrationWarning>
<body className="font-sans antialiased">
{children}
<SmoothScrollProvider>{children}</SmoothScrollProvider>
</body>
</html>
);

View File

@@ -0,0 +1,111 @@
"use client";
import { useEffect, type ReactNode } from "react";
import { useReducedMotion } from "framer-motion";
import Lenis from "lenis";
interface SmoothScrollProviderProps {
children: ReactNode;
}
const DESKTOP_POINTER_QUERY = "(hover: hover) and (pointer: fine)";
const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
export function SmoothScrollProvider({ children }: SmoothScrollProviderProps) {
const shouldReduceMotion = useReducedMotion();
useEffect(() => {
if (typeof window === "undefined") {
return;
}
const desktopPointerMedia = window.matchMedia(DESKTOP_POINTER_QUERY);
const reducedMotionMedia = window.matchMedia(REDUCED_MOTION_QUERY);
let lenis: Lenis | null = null;
let frameId = 0;
const stopLenis = () => {
if (frameId) {
window.cancelAnimationFrame(frameId);
frameId = 0;
}
if (lenis) {
lenis.destroy();
lenis = null;
}
};
const startLenis = () => {
if (lenis) {
return;
}
lenis = new Lenis({
lerp: 0.085,
smoothWheel: true,
syncTouch: false,
wheelMultiplier: 0.92,
anchors: true,
prevent: (node) => {
if (!(node instanceof HTMLElement)) {
return false;
}
return Boolean(
node.closest(
"[data-lenis-prevent], [data-radix-scroll-lock], [cmdk-list]",
),
);
},
});
const raf = (time: number) => {
lenis?.raf(time);
frameId = window.requestAnimationFrame(raf);
};
frameId = window.requestAnimationFrame(raf);
};
const syncState = () => {
if (
shouldReduceMotion ||
reducedMotionMedia.matches ||
!desktopPointerMedia.matches
) {
stopLenis();
return;
}
startLenis();
};
syncState();
const handleDesktopPointerChange = () => {
syncState();
};
const handleReducedMotionChange = () => {
syncState();
};
desktopPointerMedia.addEventListener("change", handleDesktopPointerChange);
reducedMotionMedia.addEventListener("change", handleReducedMotionChange);
return () => {
desktopPointerMedia.removeEventListener(
"change",
handleDesktopPointerChange,
);
reducedMotionMedia.removeEventListener(
"change",
handleReducedMotionChange,
);
stopLenis();
};
}, [shouldReduceMotion]);
return children;
}

View File

@@ -45,6 +45,7 @@ const CommandList = React.forwardRef<
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
data-lenis-prevent=""
className={cn("max-h-[280px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>

27
package-lock.json generated
View File

@@ -16,6 +16,7 @@
"cryptocurrency-icons": "^0.18.1",
"currency-flags": "github:vivekimsit/currency-flags",
"framer-motion": "^12.36.0",
"lenis": "^1.3.18",
"lucide-react": "^0.475.0",
"next": "^14.2.24",
"react": "^18.2.0",
@@ -4564,6 +4565,32 @@
"node": ">=0.10"
}
},
"node_modules/lenis": {
"version": "1.3.18",
"resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.18.tgz",
"integrity": "sha512-7KBl3V7vx5y1h05pu9fNFZS66I0+1eZ+zUGNNNBKtEn3BONZy+nkHWvdEe2b+zKT+6WX1x7zyOb1zbYYOs6tcg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/darkroomengineering"
},
"peerDependencies": {
"@nuxt/kit": ">=3.0.0",
"react": ">=17.0.0",
"vue": ">=3.0.0"
},
"peerDependenciesMeta": {
"@nuxt/kit": {
"optional": true
},
"react": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",

View File

@@ -18,6 +18,7 @@
"cryptocurrency-icons": "^0.18.1",
"currency-flags": "github:vivekimsit/currency-flags",
"framer-motion": "^12.36.0",
"lenis": "^1.3.18",
"lucide-react": "^0.475.0",
"next": "^14.2.24",
"react": "^18.2.0",