ScrollSmoother Parallax Page — Free GSAP Snippet
ScrollSmoother Parallax Page · Scroll · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
ScrollSmoother Parallax — Smooth Scrolling With the Native Scrollbar Intact

"Smooth scrolling" libraries traditionally hijack the wheel, fake a scrollbar, and break accessibility. GSAP's ScrollSmoother (free on the CDN since 3.13) takes the opposite approach: the browser scrolls natively — real scrollbar, keyboard, find-in-page, anchor links all intact — while the visible content is *translated* to catch up smoothly. This snippet wires a whole page through it, with per-element data-speed parallax and data-lag spring effects declared entirely in markup.
How the wrapper/content trick works
ScrollSmoother requires exactly two elements: #smooth-wrapper (which it fixes in place) and #smooth-content (the actual page, which it moves with transform: translateY). The document retains its full natural height, so the native scrollbar reflects real position — but instead of the browser jumping content per scroll event, ScrollSmoother eases the transform toward the scroll position over smooth: 1.2 seconds. Scroll hard and content glides after the scrollbar like it has mass. Because it's transform-based, everything stays compositor-friendly.
data-speed is declarative parallax
With effects: true, any element carrying data-speed moves at that multiple of scroll speed: 0.85 drifts slower than the page (appearing deeper), 1.08 outruns it (appearing closer). The three demo cards sit side by side with 1.08 / 1.0 / 0.9 — scroll and watch them shear apart, a controlled experiment in perceived depth. Crucially, ScrollSmoother auto-compensates so each element is exactly at its natural position when it's centered in the viewport; parallax offsets grow toward the viewport edges. That's why speeds don't wreck layouts the way hand-rolled parallax does.
data-lag is parallax's springy cousin
Where speed changes an element's velocity, data-lag delays it: a lag of 0.3 means the element takes an extra 0.3 seconds to catch up to where it should be, then settles. The chip row (lags 0.1 / 0.3 / 0.6) turns a scroll stop into a little cascade of settling elements — the "attached by soft springs" feel that agencies charge for, in one attribute.
smoothTouch is deliberately tiny
Touch devices already have physical momentum scrolling; stacking 1.2s of synthetic smoothing on top feels drunk. smoothTouch: 0.1 applies just enough to keep effects working on mobile while respecting the platform's native feel — or set it false to disable entirely.
It's built on ScrollTrigger, and they compose
ScrollSmoother requires ScrollTrigger and registers itself as its scroller — meaning every ScrollTrigger technique (pins, scrubs, snap) works inside a smoothed page without changes. The pinned scroll effects across this library can drop into #smooth-content as-is.
What to watch out for
position: fixed elements must live *outside* #smooth-content (inside a transformed ancestor, fixed becomes absolute — a CSS spec behavior, not a plugin bug). And because content height is virtualized against the transform, dynamically injected content needs ScrollTrigger.refresh().
Customizing it
Tune smooth (0.8 subtle, 2 dramatic), spread data-speed/data-lag across real imagery, or add ScrollSmoother.create().effects() targets dynamically. Related: scrubbed depth in scroll parallax layers, row-shear in hero parallax grid, mouse parallax in parallax hero, and native-scroll pinning in scroll sticky features.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to work out the wrapper/content mechanics from scratch. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why ScrollSmoother needs two separate elements rather than one, or how the effects: true option turns plain data-speed and data-lag attributes into center-compensated parallax without any per-element JavaScript. The same assistant can help optimize it — asking whether smoothTouch: 0.1 strikes the right balance for mobile, or how to safely handle position: fixed elements that would otherwise resolve against the transformed content wrapper instead of the viewport. It's also useful for extending the effect: ask it to combine ScrollSmoother with a pinned ScrollTrigger section elsewhere on the page, add dynamically injected content that needs a ScrollTrigger.refresh call, or build a toggle that lets users disable the smoothing entirely for accessibility. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a "ScrollSmoother parallax page" in plain HTML, CSS, and JavaScript using GSAP with its ScrollTrigger and ScrollSmoother plugins (load all three from a CDN, in that order) — do not hijack scroll events manually.
Requirements:
- Exactly two nested elements required by ScrollSmoother: an outer wrapper element and an inner content element containing all of the page's real sections (a hero, a row of cards, a text band, and an end section).
- Register ScrollTrigger and ScrollSmoother, then call ScrollSmoother.create with wrapper and content selectors pointing to those two elements, a smooth duration value controlling how many seconds the content takes to catch up to the true scroll position, effects set to true so declarative attributes are activated, and a small smoothTouch value for a lighter touch of smoothing specifically on touch devices.
- Add a data-speed attribute (a number, some above 1 and some below 1) to several elements across different sections, without writing any JavaScript per element — the effects: true option alone must make those elements parallax at their declared multiple of scroll speed, appearing to move slower or faster than the page depending on whether the value is below or above 1.
- Add a data-lag attribute (different small decimal values) to a group of sibling elements so they visibly settle into position with a staggered spring-like delay whenever the user stops scrolling abruptly, again using only the attribute with no manual JavaScript.
- Confirm the native browser scrollbar, keyboard scrolling (Page Down, arrow keys, Home/End), and anchor-link jumps all continue to work correctly, since the actual document scroll position must remain real and only the rendered content's transform is being smoothed.
- Note in a comment which kinds of elements (e.g. fixed-position navigation bars or cookie banners) must be placed outside the content element entirely, since position: fixed inside a permanently-transformed ancestor resolves differently than the CSS spec's normal viewport-relative fixed behavior.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Add the GSAP CDNsgsap, ScrollTrigger, and ScrollSmoother — order matters.
- 2Paste HTML, CSS, and JSThe wrapper/content structure is required exactly.
- 3Scroll the pageContent glides after the scrollbar with inertia.
- 4Watch the cardsdata-speed values shear them apart in depth.
- 5Stop scrolling abruptlydata-lag chips settle in a spring cascade.
- 6Add effects anywhereAny element takes data-speed or data-lag.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Philosophy: the browser keeps scrolling natively — real scrollbar, keyboard paging, anchor jumps, find-in-page — and ScrollSmoother only eases the content's transform toward the true scroll position. Hijack-style libraries intercept wheel events and synthesize scrolling, which breaks those native behaviors. It also registers itself with ScrollTrigger, so the entire pin/scrub ecosystem works unchanged.
The wrapper is fixed to the viewport (the window the user sees through) while the content element — the real page at its natural height — is translated within it. The document keeps its full height so the scrollbar stays honest, and the translate is what gets smoothed. Without that two-element split there'd be nothing to transform independently of the scroll position.
ScrollSmoother anchors every effect element to its natural position at the moment it's centered in the viewport — parallax offsets grow toward the edges and vanish at center. So a 0.9× image is exactly where CSS put it when the user looks straight at it, drifting only while entering and leaving. Hand-rolled parallax lacks that compensation, which is why it usually collides with neighbors.
Speed scales an element's scroll velocity (depth illusion); lag delays it — a data-lag of 0.3 lets the element fall behind its true position and spring back over 0.3 seconds when scrolling pauses. Speeds separate layers in space; lags separate them in time, producing that soft-spring settle when a fast scroll stops.
CSS spec: inside any transformed ancestor, fixed positioning resolves against that ancestor rather than the viewport — and #smooth-content is permanently transformed. Move genuinely fixed UI (navs, cookie bars, floating buttons) outside #smooth-content as siblings of the wrapper, where they'll behave normally while the page glides beneath them.
Create it in a mount effect — useEffect, onMounted, or ngAfterViewInit — after the wrapper/content DOM exists, and call smoother.kill() in the cleanup (plus ScrollTrigger.refresh() after route content changes). In SPAs, recreate per page or keep one instance at the layout level with content swapped inside. The required structure is two plain divs, so Tailwind handles all styling; keep fixed overlays outside the wrapper.
Yes — because the browser's real scroll position is what's driving everything, Page Down, arrow keys, Space, and Home/End all move the native scrollbar exactly as before, and ScrollSmoother just eases the visual transform to catch up. Screen readers that read document order and jump to headings or landmarks are unaffected since the DOM structure and focus order never change, only the transform applied for rendering.