The Honeymoon Phase With ScrollTrigger
I remember the first time I used GSAP's ScrollTrigger plugin. I felt like I had unlocked a superpower. Elements fading in on scroll, pinned sections, scrubbed timelines tied to scroll position — it all felt like magic. I started reaching for it on every project, regardless of what the project actually needed.
Then reality caught up with me.
I was working on a small portfolio site for a local client — five sections, some text reveals, a few images that should animate in as the user scrolled. Nothing fancy. I had wired up ScrollTrigger for all of it, and during a review session on the client's mid-range Android phone, the page felt slightly janky. The animations were fighting the main thread. The GSAP + ScrollTrigger bundle was adding over 60KB minified (not gzipped) to a project that genuinely did not need it.
That was the moment I started asking a more honest question: am I using this tool because it's the right tool, or because it's the tool I know best?
What ScrollTrigger Actually Costs You
Let me be clear before I go further — GSAP is an exceptional library. GreenSock has spent decades making it the most reliable, cross-browser animation engine on the web. ScrollTrigger itself is genuinely brilliant for complex use cases. I still use it, and I'll tell you exactly when.
But the cost is real:
- Bundle size: GSAP core plus ScrollTrigger is roughly 60–70KB minified. On shared cPanel hosting without HTTP/2 push or aggressive CDN caching, that matters.
- JavaScript dependency: Every animation is now contingent on your JS loading, parsing, and executing. CSS animations are not.
- Complexity surface area: ScrollTrigger introduces concepts like scrub, pin, pinSpacing, markers, snap, and toggleActions. That's a real cognitive overhead for something that just needs to fade in.
- Debugging overhead: When a scroll animation breaks on mobile, GSAP's abstractions make it harder to pinpoint whether the issue is the plugin, the browser, your scroll container, or a CSS overflow conflict.
None of this means GSAP is bad. It means using any abstraction carelessly has a price.
The Native Alternative Stack That Covers Most Cases
Here's what I've settled into after shipping real projects: a combination of Intersection Observer API and CSS custom properties + transitions handles the vast majority of scroll-triggered animation needs without a single byte of GSAP.
Pattern 1: Fade-In on Scroll (The Most Common Use Case)
Ninety percent of "scroll animation" requests I receive from clients are just: elements should appear as the user scrolls down. That's it. Here's how I handle it with zero dependencies:
// observer.js
const revealElements = document.querySelectorAll('[data-reveal]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target);
}
});
}, {
threshold: 0.15
});
revealElements.forEach(el => observer.observe(el));
And the CSS side:
/* animations.css */
[data-reveal] {
opacity: 0;
transform: translateY(24px);
transition: opacity 0.5s ease, transform 0.5s ease;
}
[data-reveal].is-visible {
opacity: 1;
transform: translateY(0);
}
That's it. That's the whole thing. It respects prefers-reduced-motion if you add a media query, it's readable by any developer who joins the project, and it costs nothing at runtime beyond a few milliseconds of JS parsing.
You can add staggered delays purely in CSS using transition-delay driven by a --delay custom property set inline on each element — no JS loop needed for the timing.
Pattern 2: Staggered List Reveals With CSS Custom Properties
<!-- HTML -->
<ul class="feature-list">
<li data-reveal style="--delay: 0s">Feature one</li>
<li data-reveal style="--delay: 0.1s">Feature two</li>
<li data-reveal style="--delay: 0.2s">Feature three</li>
</ul>
/* CSS */
[data-reveal] {
opacity: 0;
transform: translateY(20px);
transition:
opacity 0.45s ease var(--delay, 0s),
transform 0.45s ease var(--delay, 0s);
}
[data-reveal].is-visible {
opacity: 1;
transform: translateY(0);
}
The same IntersectionObserver from Pattern 1 drives this. No additional code. The stagger is declarative, lives in the HTML, and is immediately understandable to anyone reading the markup.
So When Should You Actually Use GSAP ScrollTrigger?
I'm not here to tell you to never use ScrollTrigger. I use it regularly — but for specific, justified reasons. Here's my personal threshold:
- Scrubbed animations: When the animation position needs to be tied directly to scroll position (parallax depth, timeline scrubbing, horizontal scroll sections). Intersection Observer can't do this. ScrollTrigger is the right tool.
- Pinned sections: When a section needs to "stick" while content animates inside it. Native CSS
position: stickyhandles some of this, but complex sequenced content reveals inside a pin are genuinely better with ScrollTrigger. - Complex sequenced timelines: When multiple elements need to animate in a choreographed sequence that depends on scroll progress — not just "entered the viewport."
- SVG path drawing or morphing: GSAP's MorphSVG and DrawSVG plugins are unmatched. If scroll is involved, ScrollTrigger pairs with them perfectly.
Notice what's missing from that list: basic fade-ins, slide-ups, scale reveals, counter animations, and simple staggered list entrances. Those do not belong on the GSAP side of the decision.
The Honest Conversation About Performance
There's a narrative in the front-end community that GSAP is more performant than CSS animations because it uses requestAnimationFrame and manually optimizes transforms. This is true in specific high-frequency, JavaScript-driven contexts — like animations driven by mouse position or complex physics-based motion.
But for scroll-triggered reveals? The browser's compositor handles CSS opacity and transform transitions on its own thread entirely. CSS transitions on these properties don't touch the main thread at all. GSAP, even when animating only transforms and opacity, still runs its ticker on the main thread to coordinate the updates.
I'm not claiming CSS always wins — performance is deeply context-dependent. But the common assumption that "GSAP = more performant" is not universally true, and it definitely isn't true for the simple cases we're talking about.
What I Actually Do on Projects Now
My current workflow looks like this:
- Start with CSS transitions and
IntersectionObserverfor all scroll reveals. Zero dependencies. - If a specific section needs complex sequenced animation or scrubbing, pull in GSAP core + ScrollTrigger for that section only, ideally loaded conditionally.
- For page-level transitions or hero animations that need tight control, GSAP core (without ScrollTrigger) is often enough.
This approach keeps my base bundle lean, makes the project easier for other developers to read, and forces me to be intentional about when animation complexity is actually adding value versus just adding weight.
The Bigger Point
Every tool in your stack should earn its place. GSAP ScrollTrigger earns its place on projects that genuinely need what it offers. But if you're importing it because you saw a cool CodePen or because it's your comfort zone, you're paying a real cost — in performance, in bundle size, in maintainability — without getting the benefit that justifies it.
The Intersection Observer API has been supported in all modern browsers since 2018. CSS transitions and custom properties are even more universal. They are not second-class citizens. In many cases, they're the professional choice precisely because they're simpler, faster to ship, and easier for anyone to maintain.
The best animation tool for a project is the one that does exactly what the project needs — and nothing more.
Next time you reach for ScrollTrigger, ask yourself honestly: does this project need scrubbing, pinning, or complex timeline orchestration? If the answer is no, you probably don't need it. Ship the leaner thing. Your users' phones will thank you.