Getting Started
Scroll Animations
Text Animations
Hover & Cursor
Sliders & Carousels
Galleries & Grids
Menus & Navigation
Preloaders & Intros
Page Transitions
3D & WebGL
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.
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.
"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.Use current option names. smooth, smoothTouch and direction are from older releases and are ignored by Lenis 1.x.
lenis.stop() while a full-screen menu or preloader is open and lenis.start() when it closes.lenis.scrollTo(0, { immediate: true, force: true }), then call ScrollTrigger.refresh() before uncovering so every trigger measures the new page.scroller propScrollTrigger 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:
scroller prop, andusePreviewScroller(), 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.
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.
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 releasesend 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.onUpdate. Timelines are easier to retime and to reverse. Keep onUpdate for maths a timeline cannot express, such as arcs, physics or exponential zooms.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.
end multipliers rather than dropping the effect.Related components: Pinned Columns Scroll, Scroll Pin Slider, Split Cards Scroll, Parallax Image Scroll.
gsap.ticker for the whole page.