skills/lenis-integration/SKILL.md
Lenis smooth scroll setup with Next.js App Router, ScrollTrigger integration, React lifecycle management, and known issues.
npx skillsauth add adilkalam/orca lenis-integrationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
npm install lenis
Create a reusable Lenis provider for the App Router:
'use client';
import { useEffect, useRef } from 'react';
import Lenis from 'lenis';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
export function LenisProvider({ children }: { children: React.ReactNode }) {
const lenisRef = useRef<Lenis | null>(null);
useEffect(() => {
const lenis = new Lenis({
lerp: 0.1, // from design-dna motion.scroll.lenisLerp
smoothWheel: true,
orientation: 'vertical',
});
lenisRef.current = lenis;
// Connect Lenis to GSAP ScrollTrigger
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add((time) => {
lenis.raf(time * 1000);
});
gsap.ticker.lagSmoothing(0);
return () => {
lenis.destroy();
lenisRef.current = null;
};
}, []);
return <>{children}</>;
}
Wrap the App Router layout:
// app/layout.tsx
import { LenisProvider } from '@/components/lenis-provider';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<LenisProvider>{children}</LenisProvider>
</body>
</html>
);
}
Lenis replaces the browser's native scroll. ScrollTrigger must be told about Lenis scroll position:
// This is the critical wiring -- without it, ScrollTrigger won't work with Lenis
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add((time) => {
lenis.raf(time * 1000);
});
gsap.ticker.lagSmoothing(0);
For custom scroll containers (not document-level), use scrollerProxy:
ScrollTrigger.scrollerProxy(container, {
scrollTop(value) {
if (arguments.length) {
lenis.scrollTo(value, { immediate: true });
}
return lenis.scroll;
},
getBoundingClientRect() {
return {
top: 0, left: 0,
width: window.innerWidth,
height: window.innerHeight,
};
},
});
Always destroy Lenis on unmount:
useEffect(() => {
const lenis = new Lenis({ lerp: 0.1 });
// ... setup ...
return () => {
lenis.destroy();
};
}, []);
Lenis persists across route changes in App Router (if in layout). Scroll position resets automatically on navigation. If you need to preserve scroll:
// Scroll to top on route change (default behavior)
lenis.scrollTo(0, { immediate: true });
| Option | Default | Description |
|--------|---------|-------------|
| lerp | 0.1 | Smoothness (0 = instant, 1 = no smoothing). Use motion.scroll.lenisLerp |
| smoothWheel | true | Enable smooth wheel scrolling |
| orientation | 'vertical' | Scroll direction |
| gestureOrientation | 'vertical' | Touch gesture direction |
| wheelMultiplier | 1 | Wheel scroll speed multiplier |
| touchMultiplier | 2 | Touch scroll speed multiplier |
| infinite | false | Infinite scrolling mode |
{
"motion": {
"scroll": {
"smoothing": true,
"lenisLerp": 0.1
}
}
}
Map smoothing: false to skip Lenis initialization entirely.
Lenis intercepts scroll. Use lenis.scrollTo('#target') instead of native anchor behavior:
const handleAnchorClick = (e: React.MouseEvent, target: string) => {
e.preventDefault();
lenis?.scrollTo(target);
};
Stop Lenis when modals are open to prevent background scrolling:
// When opening modal
lenis.stop();
// When closing modal
lenis.start();
Fixed elements work normally with Lenis. Sticky elements work but verify with ScrollTrigger pinning -- Lenis can sometimes interfere with pin calculations. Test thoroughly.
If frame rate drops below 30fps, consider:
lerp to 0.15-0.2 (less interpolation work)smoothWheel: false on mobile via matchMediaLenis must be initialized client-side only. The 'use client' directive on the provider handles this. Never import Lenis in server components.
After completing the work, before returning to the user, ask:
"Returned to bench. Anything here you'd rant about?"
If the user responds, append the entry to the current project's .orca/design-rants-pending.md in this format:
## YYYY-MM-DD HH:MM — [verb-name]
[user's response verbatim]
Create .orca/ in the current project if absent. Do NOT write to ~/.claude/ or to the ORCA-OS source tree directly. Pending entries are swept and categorized later via /impeccable extract rants.
testing
Run technical quality checks across accessibility, performance, theming, responsive design, and anti-patterns. Generates a scored report with P0-P3 severity ratings and actionable plan. Use when the user wants an accessibility check, performance audit, or technical quality review.
tools
Improves typography by fixing font choices, hierarchy, sizing, weight, and readability so text feels intentional. Use when the user mentions fonts, type, readability, text hierarchy, sizing looks off, or wants more polished, intentional typography.
tools
Three.js animation - keyframe animation, skeletal animation, morph targets, animation mixing. Use when animating objects, playing GLTF animations, creating procedural motion, or blending animations.
development
Plan the UX and UI for a feature before writing code. Runs a structured discovery interview, then produces a design brief that guides implementation. Use during the planning phase to establish design direction, constraints, and strategy before any code is written.