GSAP scroll animation and smooth scroll with Lenis have become the go-to combination for high-end web experiences in 2026 — and for good reason. The results, when done right, feel cinematic. But most tutorials stop at a basic parallax demo. When you’re building a production website with pinned sections, video scrubbing, and text reveals all running simultaneously on a WordPress stack, the gap between the tutorial and the real project is significant. This is a breakdown of what I learned building it.

The Problem: Client Wanted ‘That Premium Feel’

The brief was clear in feeling, vague in spec: ‘It should feel like the high-end agency sites. Smooth scroll, elements that animate as you scroll, the whole thing.’ The client had bookmarked a few reference sites — all using GSAP ScrollTrigger with smooth scroll. The site itself was WordPress-based, which added its own constraints.

The specific interactions they wanted:

  • Pinned horizontal scroll section for showcasing services
  • Text lines revealing word by word as the user scrolls
  • Parallax image layers at different scroll speeds
  • A background video that scrubs (plays/pauses) based on scroll position

The challenge wasn’t any single interaction — it was all four running without fighting each other, on a site that also had to load fast and work on mobile.

My Approach: Lenis First, GSAP Second

The order matters. Lenis handles the scroll physics — the momentum, the smoothing, the feel. GSAP ScrollTrigger then listens to Lenis’s scroll position rather than the native browser scroll. If you wire them up wrong, ScrollTrigger fires at the wrong positions and your animations are always slightly off.

The correct integration looks like this: Lenis emits a scroll event on every frame, and you pass that scroll data to ScrollTrigger via ScrollTrigger.scrollerProxy(). Lenis’s own documentation covers this, but the gotcha is that you also need to call ScrollTrigger.addEventListener(‘refresh’, () => lenis.resize()) — otherwise, when the page height changes (dynamic content, lazy images loading in), ScrollTrigger’s calculations go stale.

Stack

WordPress, GSAP 3 (ScrollTrigger, SplitText), Lenis 1.x, Video.js for the scrubbing section, vanilla JS (no jQuery), compiled with a simple Webpack build piped into a child theme.

The Interesting Technical Part: Video Scrubbing

Scrubbing a video based on scroll position is conceptually simple: map the scroll percentage to a point in the video’s duration, then set video.currentTime to that value. In practice, setting currentTime too frequently causes browsers to stutter — each seek triggers a decode operation, and if you’re firing 60 events per second, the video can’t keep up.

The fix I landed on:

  1. Preload the video as a series of frames using a canvas element at low resolution for mobile (this is the approach Mastercard and other enterprise sites use)
  2. For desktop, use the native video element but throttle the currentTime updates to every 3rd animation frame using a simple frame counter
  3. Use GSAP’s onUpdate callback inside ScrollTrigger rather than a scroll event listener — GSAP’s RAF loop is smarter about batching

This brought the desktop scrubbing from visibly choppy to smooth on mid-range hardware.

The Mobile Problem

Lenis smooth scroll on mobile is a contentious topic. Touch inertia already exists natively on iOS and Android, and overlaying Lenis on top of it can feel wrong — either fighting the native scroll or doubling the momentum. My approach: disable Lenis on touch devices entirely (using a pointer: coarse media query check at init), and use CSS scroll-snap for the pinned section instead of GSAP ScrollTrigger pinning. Users on touch screens get a near-identical experience without the overhead.

This also solved a nasty iOS Safari bug where fixed-position elements inside a ScrollTrigger pin would flicker during momentum scroll. Not a Lenis issue — a Safari issue — but Lenis was making it worse.

WordPress Integration Gotchas

A few things specific to running this on WordPress:

  • WordPress’s admin bar (32px height) offsets your scroll calculations. Account for it with body.admin-bar .your-pinned-section { top: 32px; } and pass the offset to ScrollTrigger’s start values
  • Third-party plugins that inject scripts (contact forms, cookie banners) can fire DOMContentLoaded before your GSAP init. Wrap your ScrollTrigger.create() calls in a window.load listener, not DOMContentLoaded
  • If you’re using Elementor or similar page builders, their script loading order is unpredictable. Load GSAP via your theme’s functions.php with a high priority, not through the builder

Results and What I’d Do Differently

The site shipped with all four interactions intact and a Lighthouse performance score above 85 on desktop (mobile was lower — 72 — mostly due to the video preload). The client was happy. Bounce rate on the homepage dropped noticeably compared to the old site.

What I’d do differently: next time I’d evaluate whether all four interactions are necessary. The word-reveal and the parallax layers competed visually. Restraint often produces a better result than maxing out every scroll trick available. The pinned horizontal scroll and the video scrubbing were the two that clients consistently pointed to as impressive — the rest was noise I’d probably cut.

Also: always build a reduced-motion fallback from day one, not as an afterthought. prefers-reduced-motion users shouldn’t get a broken layout; they should get a clean static version.