SuperAPI AI Directory

lenis-smooth-scroll

General & other

View repository

Updated 10-16 01:12

About this skill

Skill: lenis-smooth-scroll

Set up Lenis as the physics-based smooth-scroll foundation for the whole page, and synchronize it correctly with GSAP ScrollTrigger — this is the step that is most often done wrong when combining these two libraries.

WHAT

Purpose and context

  • Purpose: set up Lenis as the physics-based smooth-scroll foundation for the WHOLE PAGE and synchronize it correctly with GSAP ScrollTrigger (one raf driver, no phase offset, no jank when returning to the tab), by default using the original library verbatim at Rheinmir/uiux-asset.
  • Trigger (when to use):
  • The page needs a scroll feel that is "heavy/smooth" in the Apple/awwwards style instead of the browser's default stiff, jerky scroll.
  • The page already uses or will use GSAP ScrollTrigger (parallax, reveal, pin) and needs Lenis + ScrollTrigger to NOT conflict (jank, trigger position offset).
  • DO NOT use if you only need a few light scroll effects and do not need background smooth-scroll (use scroll-effects — vanilla, 0 dependency).
  • DO NOT use to pin/horizontally scroll a separate section (that is gsap-scrolltrigger-pin) — but if the page has both needs, initialize Lenis according to this skill FIRST, then gsap-scrolltrigger-pin works normally on top because both use ScrollTrigger as the axis.
    • User says "lenis", "smooth scroll", "apple-style smooth scrolling", "physics-based scroll", or /lenis-smooth-scroll.
  • Non-goals: not pinning/horizontally scrolling a section (→ gsap-scrolltrigger-pin), not zero-dependency individual scroll effects (→ scroll-effects), not rewriting the author's original code.

Mental model

bản gốc (Rheinmir/uiux-asset) → copy nguyên file → chỉ sửa nội dung | if external libs are not allowed: assets/ (vanilla rút gọn) → ADAPT. Technical core: reduced-motion? → không Lenis / new Lenis → driver: 2A raf tự quản (không GSAP) XOR 2B gsap.ticker (có GSAP) → lenis.on('scroll', ScrollTrigger.update) → reveal/data-speed ăn theo cùng driver.

Input and output contract

Field Required? Meaning
In target page/project yes has a build step (npm) or a static page (CDN)
In does it use GSAP ScrollTrigger? yes decides driver 2A or 2B
In are external libs allowed? yes decides the original version or assets/
Out Lenis scroll page yes a single driver; * 1000 if via gsap.ticker; lagSmoothing(0); the reduced-motion branch remains intact
Out verify evidence yes playwright-verify: fast scrolling does not jump position + reduced-motion branch passes page.emulateMedia

Rules and capabilities

  • RULE-01 (MUST): Install: npm i lenis gsap (recommended for projects with a build step) — or CDN as in assets/demo.html (unpkg.com/lenis, cdnjs.cloudflare.com/.../gsap) for static/quick-demo pages. Do not use the old Lenis version with package name @studio-freight/lenis (renamed to lenis) unless the project is pinning the old version for a reason.
  • RULE-02 (MUST): Common mistake #1: forgetting gsap.ticker.lagSmoothing(0) → scrolling is normally smooth but jerks once when the user switches tabs and comes back — because GSAP's automatic lag-smoothing stacks on top of Lenis's own smoothing mechanism (see the explanation in the Mechanism section).
  • RULE-03 (MUST): Common mistake #2: running BOTH 2A (requestAnimationFrame(raf) self-managed) AND 2B (gsap.ticker.add) at the same time → two raf loops out of phase, causing continuous slight jitter. Choose only ONE driver: if GSAP is available use 2B, otherwise use 2A.
  • RULE-04 (MUST): Common mistake #3: calling lenis.raf(time) from gsap.ticker but forgetting to multiply by * 1000 (seconds → milliseconds) → easing is computed incorrectly.
  • RULE-05 (MUST): prefers-reduced-motion: reduce → do not initialize Lenis, do not fake smoothness with CSS scroll-behavior: smooth as a replacement — leave native browser scrolling completely intact (already coded in assets/lenis-demo.js, do not remove this branch when adapting).
  • RULE-06 (MUST): Verify with playwright-verify: scroll rapidly and continuously, check that there is no position jump between data-ls-reveal and the section containing it; also test the reduced-motion branch (simulated via page.emulateMedia).
  • Capabilities: read/copy the sample file from an external repo or assets/; write into the target project; install frontend dependencies (lenis, gsap); run a headless browser to verify.

Failure boundaries

  • Cannot open/clone Rheinmir/uiux-asset → use the assets/ reduced vanilla (partial), notify the user.
  • User has prefers-reduced-motion: reduce enabled → do not initialize Lenis (correct behavior, not a bug).
  • Verification finds position jumps/jitter → failed, re-check the 3 common mistakes (#1–#3) before reporting completion.
  • Project pins @studio-freight/lenis for a reason → keep it as is, clarify with the user before changing the package.

HOW

Main workflow

Step Type Inputs Action Outputs/exit Failure/next
W01 judgment request Choose the appropriate original pen from the "Original" table, open the link run to see the correct effect chosen pen must not pull in external libs → B01
W02 effect pen Copy the original file .html (+ _assets/) into the project, keep the attribution comment, only edit content/images/colors; run over HTTP working page —
W03 deterministic page Install (npm i lenis gsap or CDN), initialize Lenis after the reduced-motion guard; pick ONE driver (2A or 2B) Lenis runs with GSAP → B02
W04 deterministic page Verify with playwright-verify (fast scroll + reduced-motion via page.emulateMedia) pass jerk/jump → check errors #1–#3, repeat W03

Step-by-step details (source of truth for W01–W04):

Original — use FIRST (copy the file as-is, don't rewrite)

The author's VERBATIM original code (public CodePen = MIT) is in the external repo Rheinmir/uiux-asset — live gallery: https://rheinmir.github.io/uiux-asset/scroll-effects/ . Process: open link runs to see the exact effect → copy the file as-is .html (+ folder _assets/ if the file references it) into the project → keep the attribution comment on the first line → only change content/images/colors. Run locally over HTTP, not file://. git clone --depth 1 https://github.com/Rheinmir/uiux-asset if you need the full offline set.

Original Pen — author Link
Lenis Smooth Scroll & GSAP Page — filipz runs · code
Lenis Smooth Scroll Cinematic Experience — filipz runs · code
Layout Explorations with GSAP, Flip, Lenis and ScrollTrigger N°2 — filipz runs · code
Staggered Text Scroll Reveal — gusevdigital runs · code

assets/ in this skill is only a self-written vanilla condensed version — use when the project CANNOT pull external libs; by default use the original above.

Assets

assets/demo.html + assets/lenis-demo.css + assets/lenis-demo.js — a working demo page (Lenis CDN + GSAP), read then ADAPT (change tokens/sections per project, switch CDN to npm bundle if the project already has a build step).

Mechanism

1. Initialize Lenis
const lenis = new Lenis({
  lerp: 0.1,        // nội suy mỗi frame (0–1): thấp = mượt/trễ hơn, cao = bám sát input hơn
  duration: 1.2,     // easing khi có target (vd anchor scroll), không áp dụng cho wheel liên tục
  smoothWheel: true, // bật vật lý mượt cho wheel/trackpad — lý do chính để dùng Lenis
});
2A. Standalone (no GSAP) — self-managed raf loop
function raf(time) {
  lenis.raf(time);
  requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
2B. With GSAP ScrollTrigger — MERGE into a single driver (do not use both 2A and 2B at the same time)
gsap.registerPlugin(ScrollTrigger);

lenis.on('scroll', ScrollTrigger.update);         // Lenis cuộn -> báo ScrollTrigger cập nhật ngay
gsap.ticker.add((time) => lenis.raf(time * 1000)); // gsap.ticker LÀM vòng raf cho Lenis luôn
gsap.ticker.lagSmoothing(0);                        // tắt bù-lag mặc định của GSAP (xem lý do bên dưới)

Why these 3 lines must be in exactly this order (the most common mistake):

  • gsap.ticker already runs its own requestAnimationFrame loop internally to drive all tweens/ScrollTriggers. If Lenis runs its own extra requestAnimationFrame(raf) (approach 2A) WHILE GSAP is already present, the two loops are out of sync — each frame they update a few milliseconds apart, and ScrollTrigger reads a scroll position "older" or "newer" than Lenis by one frame. The feel is a constant slight jerk when scrolling fast, especially in sections with pin or scrub. Fix it by letting gsap.ticker be the ONLY driver: remove the separate requestAnimationFrame(raf) , replace it with gsap.ticker.add(...).
  • gsap.ticker emit time in seconds, while lenis.raf() needs milliseconds → must multiply by * 1000. Forgetting to multiply is a silent error: Lenis runs but easing/velocity are computed wrong on a time scale 1000 times smaller, scroll looks "stuck" or "flying" depending on lerp.
  • lenis.on('scroll', ScrollTrigger.update) is mandatory because Lenis updates scroll position via internal transform (lerp each frame, not scrollTop instantly) — if not reported, ScrollTrigger still uses the browser's native scroll event to compute pin/progress, which does NOT match the "smooth" position Lenis is rendering → pin drifts by a few px or triggers fire at the wrong time.
  • gsap.ticker.lagSmoothing(0) handles a different bug, easily missed: by default GSAP auto-detects abnormally delayed frames (e.g., a tab loses focus for a few seconds then returns) and "compresses" time to compensate, so animations don't jump. But Lenis already has its own delta-time smoothing mechanism — two time-compensation systems stacked cause the tab-return to jitter noticeably instead of being smooth. Disable GSAP's compensation entirely (lagSmoothing(0)) so Lenis alone handles that part.
3. Respect prefers-reduced-motion: reduce

Do not "fake-smooth" for users who have reduced motion enabled — do not initialize Lenis at all, let the browser use native scroll (normal jump-scroll). Check BEFORE calling new Lenis(...):

if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
  // new Lenis(...) + sync ở trên
} else {
  // cuộn gốc trình duyệt; nếu có reveal-on-scroll đi kèm, hiện thẳng bằng CSS/class, không chờ cuộn mượt
}

See the full application (with reveal fallback demo) in assets/lenis-demo.js.

4. Demo: reveal + data-speed when scrolling via Lenis

assets/demo.html has a few sections with data-ls-reveal (staggered fade-in, using ScrollTrigger.create({ once: true }) tied to scroll already synced in step 2B) and data-speed="0.12" (layer drifting slightly via gsap.to(..., { scrollTrigger: { scrub: true } }), value clamped |v| ≤ 0.3 to avoid motion sickness) — both use the SAME established Lenis/GSAP driver, no separate scroll listener.

Branches

ID Kind Guard Behavior Skip / failure Rejoin
B01 conditional_required project must NOT pull external lib / needs a self-written version use assets/demo.html + assets/lenis-demo.css + assets/lenis-demo.js, ADAPT token/section — W03
B02 conditional_required page has GSAP ScrollTrigger driver 2B: lenis.on('scroll', ScrollTrigger.update) + gsap.ticker.add((time) => lenis.raf(time * 1000)) + gsap.ticker.lagSmoothing(0); drop 2A entirely no GSAP → 2A W04
B03 conditional_required prefers-reduced-motion: reduce no new Lenis, native scroll; reveal shows directly via CSS/class — W04
B04 user_optional page needs additional pin/horizontal-scroll section initialize Lenis per this skill FIRST then use gsap-scrolltrigger-pin above — W04

Validation and stopping

Done when playwright-verify scrolls rapidly and repeatedly without position jumps between data-ls-reveal and the section containing it, and the reduced-motion branch (via page.emulateMedia) the original scroll does not use Lenis. The code has only ONE raf driver.

Examples

  • Positive: "make an apple-style smooth-scrolling landing page with parallax" → choose the pen "Lenis Smooth Scroll & GSAP Page — filipz", copy it as is lenis-smooth-scroll-gsap-page.html, change content/images → verify fast scrolling does not misalign the pin, reduced-motion uses the original scroll.
  • Boundary/failure: the adapted code has both requestAnimationFrame(raf) and gsap.ticker.add(...) → slight continuous jitter (bug #2) → remove loop 2A, keep only 2B, verify again; forget * 1000 then scrolling "freezes/drifts" (bug #3).

Origin

Batch 180926 — the original VERBATIM version is at Rheinmir/uiux-asset/scroll-effects/ (see the "Original" section above); assets/ is a trimmed-down vanilla version written by hand.