tween
ComponentsTemplatesShowcaseContact

Command Palette

Search for a command to run...

Getting Started
  • Introduction
  • Installation
  • StoryNew
Guides
  • Motion Principles
  • Scroll and Smooth Scroll
  • Performance and Accessibility
Scroll Animations
  • Sticky Cards Scroll
  • Rise Fan Cards Scroll
  • Rotate Stack Scroll
  • Doodle Lens ScrollNew
  • Slit Open Hero ScrollNew
  • Logo Mask Zoom ScrollNew
  • ASCII Dissolve ScrollNew
  • Lottie Flight ScrollNew
  • Inline Icon Text ScrollNew
  • Starfield ScrollNew
  • Name Swap ScrollNew
  • Spotlight Gallery HeroNew
  • Slit Rotate Hero ScrollNew
  • Stroke Cover ScrollNew
  • Stroke Rows ScrollNew
Text Animations
  • Text Skew Reveal
  • Block Text Reveal
Hover & Cursor
  • Image Trail Cursor
  • Cursor Depth Logo
  • Physics Tag Drop
  • Elastic Card ListNew
  • Magnetic Cards SpringNew
Sliders & Carousels
  • Wheel Fullscreen Carousel
  • Ripple Displacement Slider
  • Three.js Blend Slider
  • Stacked Deck Slider
  • Orbit Cylinder SliderNew
  • Fan Stack SliderNew
  • Exponential Strip SliderNew
  • Dual Column SliderNew
Galleries & Grids
  • Scroll Wave Gallery
Menus & Navigation
  • Shrink Page MenuNew
Preloaders & Intros
  • Floating Items RevealNew
  • Word Cycle RevealNew
  • Tile Grid LoaderNew
Page Transitions
  • Panel Assembly TransitionNew
3D & WebGL
  • Fractal Glass Hero
  • Fluid Cursor Canvas
  • Fluid Portrait Hero
  • Fluid Gradient Canvas
  • Inversion Lens HoverNew
UI Elements
  • Circular Widgets
  • Vinyl Cover
  • Image Explosion Footer
  • Accordion Image FramesNew

(Tween) — Motion registry

Animated components & full sites, one command away.

Product

  • Components
  • Templates
  • Installation
  • Guides
  • Blog
  • Contact

Categories

  • Scroll Animations
  • Text Animations
  • Hover & Cursor
  • Sliders & Carousels
  • Galleries & Grids
  • Menus & Navigation
  • Preloaders & Intros
  • Page Transitions
  • 3D & WebGL
  • UI Elements
TWEEN
ease: power2.inOut0.00s
© 2026 TweenBack to topBuilt by mohammad
  1. Docs/
  2. Guides/
  3. Scroll and Smooth Scroll

Scroll and Smooth Scroll

PreviousNext

Wiring Lenis to ScrollTrigger, pinning sections, and supporting nested scrollers.

Most Tween components are driven by ScrollTrigger. This guide covers how to add smooth scrolling without breaking them, how to structure pinned sections, and how the scroller prop lets the same component run inside the page or inside a scrolling container.

One Lenis, one ticker

Run a single Lenis instance per scroll container and drive it from GSAP's ticker, so smooth scroll and every tween advance on the same frame.

pnpm add lenis gsap
components/smooth-scroll.tsx
"use client"
 
import * as React from "react"
import { gsap } from "gsap"
import { ScrollTrigger } from "gsap/ScrollTrigger"
import Lenis from "lenis"
 
if (typeof window !== "undefined") {
  gsap.registerPlugin(ScrollTrigger)
}
 
export function SmoothScroll() {
  React.useEffect(() => {
    const reduceMotion = window.matchMedia?.(
      "(prefers-reduced-motion: reduce)"
    ).matches
    if (reduceMotion) return // keep native scrolling
 
    const isTouch = window.matchMedia("(pointer: coarse)").matches
 
    const lenis = new Lenis({
      duration: isTouch ? 0.8 : 1.2,
      easing: (t: number) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
      syncTouch: isTouch,
      touchMultiplier: 1.5,
    })
 
    // Tell ScrollTrigger whenever Lenis moves the page.
    const onScroll = () => ScrollTrigger.update()
    lenis.on("scroll", onScroll)
 
    // Drive Lenis from GSAP's clock instead of its own loop.
    const tick = (time: number) => lenis.raf(time * 1000)
    gsap.ticker.add(tick)
    gsap.ticker.lagSmoothing(0)
 
    return () => {
      lenis.off("scroll", onScroll)
      gsap.ticker.remove(tick)
      lenis.destroy()
    }
  }, [])
 
  return null
}

Mount it once, high in your layout. Three details matter:

  • ScrollTrigger.update on every Lenis scroll event. Without it, scrubbed animations lag a frame behind or jump.
  • lagSmoothing(0). GSAP normally compensates for dropped frames by slowing time; with smooth scroll, that makes the page drift away from the finger or wheel after a long frame.
  • Respect reduced motion. Users who ask for less motion get native scrolling. Do not smooth-scroll them anyway.

Use current option names. smooth, smoothTouch and direction are from older releases and are ignored by Lenis 1.x.

Controlling scroll from other components

  • Overlays. Call lenis.stop() while a full-screen menu or preloader is open and lenis.start() when it closes.
  • Route changes. Under a transition cover, reset with lenis.scrollTo(0, { immediate: true, force: true }), then call ScrollTrigger.refresh() before uncovering so every trigger measures the new page.
  • Share the instance through context or a small store rather than creating a second Lenis somewhere else. Two instances on one container fight for the scroll position.

Nested scrollers and the scroller prop

ScrollTrigger watches the window by default. When a component lives inside its own scrolling element (a modal, a split pane, the Tween preview frame), every trigger has to name that element as its scroller.

Tween components handle this with two inputs:

  1. an explicit scroller prop, and
  2. an ambient scroller from usePreviewScroller(), which the docs preview provides.

The explicit prop wins; if neither is set, ScrollTrigger falls back to the window.

"use client"
 
import * as React from "react"
import { gsap } from "gsap"
import { ScrollTrigger } from "gsap/ScrollTrigger"
 
import { usePreviewScroller } from "@/registry/lib/preview-scroller"
 
if (typeof window !== "undefined") {
  gsap.registerPlugin(ScrollTrigger)
}
 
export function FadeUpSection({
  children,
  scroller,
}: {
  children: React.ReactNode
  scroller?: HTMLElement | string | null
}) {
  const ref = React.useRef<HTMLElement>(null)
  const ambientScroller = usePreviewScroller()
  const activeScroller = scroller ?? ambientScroller
 
  React.useEffect(() => {
    const root = ref.current
    if (!root) return
 
    const reduceMotion = window.matchMedia?.(
      "(prefers-reduced-motion: reduce)"
    ).matches
    if (reduceMotion) return
 
    const ctx = gsap.context(() => {
      gsap.from(root.querySelectorAll("[data-fade]"), {
        y: 40,
        autoAlpha: 0,
        duration: 1,
        stagger: 0.1,
        ease: "power3.out",
        scrollTrigger: {
          scroller: activeScroller ?? undefined,
          trigger: root,
          start: "top 75%",
          once: true,
        },
      })
    }, root)
 
    return () => ctx.revert()
  }, [activeScroller])
 
  return <section ref={ref}>{children}</section>
}

Pass activeScroller ?? undefined to every trigger in the component, including pins and ScrollTrigger.create calls, and list it in the effect's dependencies so triggers are rebuilt if the container changes.

Smooth scroll inside a container

Lenis can smooth a single element instead of the window. Give it the scrolling element as wrapper and its direct child as content, and wire it to ScrollTrigger exactly as above:

const lenis = new Lenis({
  wrapper: scrollerEl, // the element with overflow-y: auto
  content: contentEl, // its scrolling child
  duration: 1,
  allowNestedScroll: true, // let inner scroll areas keep native wheel
})

Then pass scrollerEl down as the scroller prop (or through context) to the components inside. After the container is resized or opened, call ScrollTrigger.refresh() on the next animation frame so positions are measured against the new size.

Pinned sections

The grammar

A pinned scene sits between a calm intro and outro, each 100svh:

intro (100svh)  →  pinned scene (N × viewport of scroll)  →  outro (100svh)

The pin itself uses a small, consistent set of options:

const tl = gsap.timeline({
  defaults: { ease: "none" },
  scrollTrigger: {
    scroller: activeScroller ?? undefined,
    trigger: section,
    start: "top top",
    end: () => `+=${window.innerHeight * 4}`,
    pin: true,
    pinSpacing: true,
    scrub: 1,
    invalidateOnRefresh: true,
  },
})
 
tl.addLabel("open")
  .to(frame, { scale: 1, borderRadius: 0 }, "open")
  .addLabel("caption")
  .from(caption, { yPercent: 100, ease: "power3.out" }, "caption")
  .addLabel("rest")
  .to({}, { duration: 0.3 }) // hold the final state before the pin releases
  • end as a function plus invalidateOnRefresh: true keeps the scroll length correct after a resize or orientation change.
  • scrub: 1 adds one second of catch-up, which smooths wheel steps even without Lenis.
  • Prefer a scrubbed timeline with labels over computing every value by hand in onUpdate. Timelines are easier to retime and to reverse. Keep onUpdate for maths a timeline cannot express, such as arcs, physics or exponential zooms.
  • End on a hold. A short empty tween at the end gives the finished composition a moment on screen before the pin releases.

Stacks

For stacking cards, pin each card with pinSpacing: false so the next card slides over it, and scrub only transform on an inner element. Sticky Cards Scroll is the reference implementation: one pin per card from top 35% to the outro, and a scrubbed y on the card's inner wrapper so earlier cards creep upward and their edges stay visible.

CSS position: sticky plus a progress-only ScrollTrigger is a lighter alternative when you do not need GSAP to manage the pin.

Budgets

  • At most about 15 viewports of pinned scroll per page.
  • At most three pinned sections back to back; put an unpinned section between longer runs.
  • On phones, shorten end multipliers rather than dropping the effect.

Related components: Pinned Columns Scroll, Scroll Pin Slider, Split Cards Scroll, Parallax Image Scroll.

Combining sections

  • One Lenis instance and one gsap.ticker for the whole page.
  • Each pinned scene ends on a resting state before the next one starts.
  • Components that take over the wheel or the scroll length entirely (tunnels, infinite scrollers) belong on their own route, not between other pinned scenes.

Next: Performance and Accessibility.

Motion PrinciplesPerformance and Accessibility

On This Page

One Lenis, one tickerControlling scroll from other componentsNested scrollers and the scroller propSmooth scroll inside a containerPinned sectionsThe grammarStacksBudgetsCombining sections

Contribute

  • Report an issue
  • Request a feature
  • Edit this page