Chapter 09 of 11Part 3 · GSAP
Scroll animation with GSAP and CSS
Tie motion to scroll with ScrollTrigger and CSS scroll-driven animations, and know when smooth scrolling is worth it.
11 minBy Zuzo StudioUpdated
In this chapter you will
- tell a triggered scroll animation from a scrubbed one and pick the right kind
- set up ScrollTrigger with trigger, start, end, scrub and a careful pin
- write a CSS scroll-driven animation that degrades safely where it is not supported
Before you startThis builds on The GSAP timeline: sequencing a hero intro.
Scrolling is the one thing every visitor does on every page, which makes it tempting to hang motion on it. A good scroll animation answers the reader: a section settles into place as it arrives, a progress bar fills as you read. A bad one fights the reader: content that slides in too late to read, or a page that scrolls at a speed you did not choose. This chapter covers the two ways to build it today, GSAP's ScrollTrigger plugin and CSS scroll-driven animations, and when a smooth scrolling library is worth adding. It builds on the timelines from the previous chapter.
01Two kinds of scroll animation
Before any code, decide which kind you are building, because it changes every choice after it.
- Triggered. The animation plays on its own clock once an element reaches a point on screen. Scroll only decides when it starts. A card that rises and fades in over 500ms when it enters the viewport is triggered.
- Scrubbed. The animation has no clock. Its progress is tied to the scroll position, so scrolling half way through a section moves the animation half way, and scrolling back plays it backwards. A reading progress bar is scrubbed.
Triggered animation suits content reveals, because the reader gets the finished state quickly whatever speed they scroll at. Scrubbed animation suits things that show position or tell a story step by step. Easing works differently too: on a triggered animation you choose an ease as in easing and timing, but on a scrubbed one the scroll is already the input, so a linear mapping usually feels most direct.
Scroll the panel below. The bar and the shape follow your scroll position in "scrubbed" mode. Switch to "triggered once" and the same movement plays on its own clock the first time you scroll past a trigger point, about a third of the way down, whatever you do after that. The percentage shows how far through the panel you are.
A scroll-driven animation maps scroll position to animation progress: halfway down the scroll range, the animation is halfway through.
02ScrollTrigger basics
ScrollTrigger is a GSAP plugin. You load it after GSAP, register it once, then add a scrollTrigger object to any tween or timeline. The ScrollTrigger documentation lists every option; four of them do most of the work.
gsap.registerPlugin(ScrollTrigger);
gsap.from('.feature', {
y: 40,
opacity: 0,
duration: 0.6,
stagger: 0.1,
scrollTrigger: { trigger: '.features', start: 'top 80%', once: true },
});
trigger is the element whose position on screen decides when things happen. It does not have to be the element that moves. Here the whole .features section is the trigger and each .feature card inside it is animated.
start and end each take two words: a place on the trigger, then a place on the viewport. 'top 80%' means "when the top of the trigger reaches 80% of the way down the viewport". The docs give the defaults as 'top bottom' for start and 'bottom top' for end, so with no settings an animation starts as soon as the element peeks in from below. Starting a little higher, around 80%, means the reader actually sees the movement instead of it finishing below the fold.
once: true plays the animation the first time and then removes the ScrollTrigger so it stops listening. Without it, the toggleActions option decides what happens when you scroll past and back; its default, 'play none none none', also plays only once but keeps the trigger alive.
scrub turns a triggered animation into a scrubbed one. scrub: true links progress straight to the scroll position. A number, such as scrub: 0.5, is the time in seconds the animation takes to catch up with the scrollbar, which smooths out jerky mouse wheels.
gsap.to('.progress', {
scaleX: 1,
ease: 'none',
scrollTrigger: { trigger: '.story', start: 'top top', end: 'bottom bottom', scrub: 0.3 },
});
Pin with care
pin: true holds the trigger element in place while the rest of the page scrolls past, for the distance between start and end. It is how those "the product spins while the text changes" sections are made. ScrollTrigger adds padding below a pinned element so the content after it does not slide underneath, and the docs warn against animating the pinned element itself, because that throws off its measurements. Animate its children instead.
Pinning takes over the reader's scroll for a while, so use it sparingly. A pinned section on a phone can fill the whole screen with one thing for several flicks of the thumb, and readers who want to skip past it cannot. Keep the pinned distance short, a screen or less, and test on a real phone.
03CSS scroll-driven animations
Browsers can now drive an ordinary CSS keyframe animation from scroll instead of time, with no JavaScript. You write the @keyframes as in chapter 5, then set animation-timeline to one of two functions:
scroll()ties the animation to how far a scroll container, by default the nearest one, has been scrolled.scroll(root)uses the page itself. This is the reading progress bar.view()ties the animation to how far an element has travelled through the visible area of its scroller, from the moment it starts entering to the moment it has fully left.
The animation-range property narrows that window. entry 0% entry 100% means "run from when the element starts entering until it has fully entered".
@keyframes rise {
from { opacity: 0; transform: translateY(2rem); }
}
@supports (animation-timeline: view()) {
.feature {
animation: rise linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
}
Order matters here. MDN's animation-timeline reference explains that the animation shorthand resets animation-timeline to auto, so you must declare animation-timeline after the shorthand or it is silently ignored.
These are scrubbed animations by nature: scroll back and they run backwards. Chrome for Developers points out the other big advantage: because they build on CSS animations, they can run off the main thread, so a busy script does not make them stutter. That is the same reason we animate only transform and opacity in chapter 6.
Browser support, October 2026
MDN marks scroll-driven animations as Limited availability, not Baseline, because one major browser is missing. Its compatibility data lists animation-timeline in Chrome and Edge from version 115 and in Safari and Safari on iOS from version 26. Firefox supports it only in desktop preview builds, not in the released browser, and not at all on Android.
So treat CSS scroll-driven animation as an enhancement. Wrap it in @supports (animation-timeline: view()), as above, and make sure the element looks finished without it. A Firefox reader then simply sees the content in place, which is a perfectly good page. This is progressive enhancement applied to motion.
| ScrollTrigger | CSS scroll-driven | |
|---|---|---|
| Triggered (plays on its own clock) | Yes | No, always scrubbed |
| Scrubbed | Yes | Yes |
| Pinning | Yes | No, use position: sticky |
| Needs JavaScript | Yes, two scripts | No |
| Works in every current browser | Yes | No, see above |
04Smooth scrolling libraries
Many award style sites use a smooth scrolling library, and the one you will meet most is Lenis. According to its README, it wraps the browser's own scroll rather than replacing it, so position: sticky, anchor links and the native scrollbar keep working. What it changes is the feel: wheel and trackpad input is eased towards its target instead of jumping, and that keeps scroll-linked effects, especially WebGL scenes, in step with the page. On touch devices it leaves native scrolling alone unless you turn on its syncTouch option, and by default it switches smoothing off for readers who ask for reduced motion.
That is a real benefit for a site where a 3D scene or a long scrubbed story must move in lockstep with the page. For most marketing sites and every content site, skip it:
- It changes something readers have strong habits about. Some people find eased wheel scrolling floaty or slow.
- It is more JavaScript on every page, running on every frame while the page scrolls.
- Scrollable areas inside the page, such as a code block or a modal, need extra setup so they scroll on their own.
If ScrollTrigger and CSS animations already look right with native scrolling, you do not need it. The full list of scroll performance checks, including why scroll event handlers that read layout cause jank, is in our website animation performance checklist.
05Choosing between them
A simple rule that holds for most projects:
- A reveal that plays when something arrives: ScrollTrigger with
once: true, or a CSS transition triggered by adding a class from anIntersectionObserverif you are not loading GSAP at all. - A progress bar or a gentle scrubbed effect that is fine to lose in one browser: CSS scroll-driven animation inside
@supports. - A pinned or multi-step scrubbed sequence: a ScrollTrigger on a GSAP timeline, so the steps can overlap with the position parameter from the last chapter.
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrollTrigger.min.js"></script>
<script src="script.js"></script>
Check your understanding
3 questions
Key takeaways
- A triggered animation plays on its own clock when an element arrives; a scrubbed one follows the scroll position, forwards and backwards.
- In ScrollTrigger, start and end each name a place on the trigger and a place on the viewport, such as "top 80%".
- Pinning takes over the reader's scroll, so keep pinned distances short and never animate the pinned element itself.
- CSS animation-timeline with view() or scroll() needs no JavaScript, but Firefox has not shipped it, so wrap it in @supports and keep the finished state as the default.
- Smooth scrolling libraries such as Lenis help keep WebGL in sync with scroll; most sites are better with native scrolling.
Sources
- ScrollTriggerGSAP, checked
- animation-timeline CSS propertyMDN Web Docs, checked
- Animate elements on scroll with scroll-driven animationsChrome for Developers, checked
- Lenis READMEdarkroom.engineering on GitHub, checked