Cookbook — Lenis (Smooth Scroll) · cb-lenis
Owner directive (2026-07-10), canonical. "generate with the Holonprint Loop with gaussian splatting, lenis, tailwind, react, next … scroll video control." [src: wiki/development/prompts-archive/by-topic/knowledge-management-and-concept-modeling.md:498]
Official docs
| item | pin | reference |
|---|---|---|
lenis | 1.3.25 [src: HOS-Instance/packages/engine/package.json:19] | https://github.com/darkroomengineering/lenis |
Our proven patterns
- gsap.ticker drives
lenis.raf. One clock:gsap.ticker.add((t) => lenis.raf(t * 1000))withgsap.ticker.lagSmoothing(0)— never a second rAF loop competing with the renderer. [src: HOS-Instance/packages/engine/src/engine/scroll.ts:76] - Emit progress from the Lenis
scrollevent, not from a scrub trigger.pageProgress = l.scroll / l.limit; Lenis's lerp already provides the scrub-lag feel. An animation-less scrub ScrollTrigger does not reliably fireonUpdate(the Lenis-emit gotcha). [src: HOS-Instance/packages/engine/src/engine/scroll.ts:60] - Reduced motion: never create Lenis. With
prefers-reduced-motion, native scroll feeds the same progress emitter via a passivescrolllistener — identical events, no smoothing. [src: HOS-Instance/packages/engine/src/engine/scroll.ts:64] scrollToChapterdual path. Smooth path useslenis.scrollTo(el, { offset, duration, immediate }); the reduced path computes the rect and jumps withbehavior: 'auto'. [src: HOS-Instance/packages/engine/src/engine/scroll.ts:100]- Scroller resolution in app shells. In both HOS apps the page scrolls inside a nested
<main class="flex-1 overflow-y-auto">, NOT window/body — Lenis drives that element and every ScrollTrigger must use it asscroller([data-scroll-root], main). [src: HOS-Webdoc/web/src/lib/scroll/scroller.ts:1] - QA hook convention. Expose the controller on
window(window.lenis/window.__st) so headless QA can read internal progress and listener counts. [src: HOS-Instance/packages/engine/src/engine/scroll.ts:86] - Tuning that shipped:
duration: 1.15, exponential ease-out,smoothWheel: true,touchMultiplier: 1.4. [src: HOS-Instance/packages/engine/src/engine/scroll.ts:66]
Gotchas
- Standalone spatial sites (Vite, window-scrolled) and app shells (Next, nested-
<main>-scrolled) resolve scrollers differently — a module hard-codingwindowbreaks inside the shell. Always resolve viaresolveScroller()there. [src: HOS-Webdoc/web/src/lib/scroll/scroller.ts:14] - Guard
l.limit > 0before dividing — a not-yet-laid-out page has limit 0. [src: HOS-Instance/packages/engine/src/engine/scroll.ts:74]
Budgets
Scroll-driven work must stay inside the frame budget of bm-001-web-perf.
Composition
- uses: cb-gsap (shared ticker + ScrollTrigger.update)
- used-by: pb-001-spatial-site-loop (step 7) · cb-patterns-royal-orbit · cb-patterns-hosweb