GSAP vs CSS vs the Web Animations API: when the JS engine wins
CSS transitions and the Web Animations API are declarative front-ends to a browser engine that can hand transform and opacity animations to the compositor thread entirely off the main thread, while GSAP runs every tween through its own JavaScript ticker on the main thread — so the real choice is never "which library is better" but whether an animation's requirements are cheap enough for the compositor's narrow contract or complex enough to need main-thread orchestration.
GSAP vs CSS vs the Web Animations API: when the JS engine wins
Seven lessons in, you've built up the machinery behind two genuinely different animation systems without stopping to compare them directly. CSS transitions, CSS @keyframes, and element.animate() are all declarative front-ends to the Web Animations model — a browser-native engine that, for the narrow but common case of animating transform and opacity, can hand the entire animation to the compositor thread and never touch the main thread again until it's done. GSAP is a JavaScript library that runs every tween through its own ticker — one requestAnimationFrame loop, on the main thread, computing new values and writing them to the DOM every frame for as long as the animation runs.
Neither of those is the "better" engine in the abstract. This lesson is the payoff of the whole module: it takes everything from lessons one through seven — interpolation, easing, the property model, timelines, transforms, scroll-scrubbing — and asks, for each capability, whether CSS/WAAPI can express it at all, and at what cost GSAP buys the cases where they can't.
What CSS and WAAPI do well
For a genuinely large share of real-world motion — a button's hover state, a modal fading in, a menu sliding open — CSS transitions and @keyframes are not just adequate, they're the better tool, for a reason this module has already built from the ground up: when an animation only ever touches transform and opacity, the browser can determine up front that no frame of it will ever need style recalculation, layout, or paint, and it hands the whole thing to the compositor thread as a self-contained instruction. That animation keeps running smoothly even while the main thread is completely blocked by something else — a guarantee no main-thread-driven engine, GSAP included, can make.
CSS and WAAPI are also the simpler, more honest tool for what they're built for: a static start state, a static end state (or a fixed sequence of keyframes), and a duration and easing curve to get from one to the other. No JavaScript has to run per frame at all for the compositor-eligible case — the browser's own C++ animation code advances the value, off-thread. WAAPI specifically adds real imperative control on top of that — pause(), currentTime, playbackRate, a finished promise — recovering most of what used to look like a GSAP-only advantage, as the WAAPI lesson covered in detail.
What they can't do easily
Several categories of real animation work fall outside what static CSS syntax — or even WAAPI's more flexible but still fundamentally single-effect model — can express cleanly, and each one maps onto a lesson earlier in this module:
- Complex, interdependent sequencing. A CSS animation is one effect with one timing function. Coordinating a dozen elements with staggered starts, overlapping labels, and nested sub-sequences that can be paused, reversed, or re-scrubbed as one unit is exactly what timelines as a data structure are for — a timeline is itself a tween whose value is a playhead position, and nothing in the CSS or WAAPI model has an equivalent "a tween made of other tweens" primitive.
- Physics and custom eases. A CSS
cubic-bezier()can only ever describe a monotonic curve — it fundamentally cannot express an ease that overshoots and settles, like elastic or bounce, because a bezier curve of that kind isn't a mathematical bezier at all. Eases in GSAP covers exactly this: GSAP's elastic and bounce eases are custom functions, not beziers, and that category of motion simply isn't representable in CSS's easing model, full stop. - Non-DOM targets. CSS and WAAPI only ever animate DOM elements' style properties. A plain JavaScript object, a value driving a canvas redraw, a uniform feeding a WebGL shader — none of these are things CSS can touch, because there's no element and no style property involved. How GSAP animates anything covered why GSAP's core tween works on
target[prop]in general, with the DOM as just one supported target among many. - Reading and decomposing existing transforms, and relative tweens.
gsap.to(el, { rotation: "+=90" })requires knowing the element's current rotation, which for an element GSAP hasn't touched yet means parsing its computedmatrix()back into components — matrix decomposition. CSS has no concept of "the current value of one component of my transform" to read back at all; you'd have to track and restate the wholetransformstring yourself. - Scroll-scrubbing. WAAPI's
ScrollTimelinegets partway here by letting a timeline's clock be scroll position instead of wall time, but it doesn't give you pinning, spacer management, ortoggleActions-style boundary callbacks out of the box — the whole apparatus ScrollTrigger under the hood covered. - Morphing and fine, dynamic, mid-flight control. Changing an animation's target values in response to something that happens while it's running — not just pausing or changing speed, but re-aiming the tween itself, or building the animation's parameters from live data at the moment it starts — is native to a JavaScript-driven tween and awkward-to-impossible to express as a static CSS rule.
The honest cost: GSAP pays for all of this on the main thread
None of the above is free, and the module has been honest about the trade the whole way through. GSAP's ticker is one requestAnimationFrame callback that computes new values and writes them to the DOM every frame, for every active tween and timeline, for the animation's entire duration — and every part of that work happens on the main thread, competing for the exact 16.6ms-per-frame budget your application's own JavaScript, style, layout, and paint work are also fighting over. A compositor-run CSS transform has no such dependency: it was handed off once, and the compositor advances it on its own schedule regardless of what the main thread is doing.
The practical consequence: if the main thread is busy — a heavy synchronous computation, a slow React render, a long event handler — a GSAP-driven animation can visibly stutter or freeze for exactly as long as the main thread stays blocked, the same way any main-thread JavaScript would. A CSS transform transition running on the compositor keeps gliding through that same block, untouched. This isn't a bug or a GSAP inefficiency to be optimized away — it's the structural price of running the tween's logic in JavaScript at all, which is the same JavaScript everything else on the page has to share a thread with. GSAP still ultimately writes to compositor-friendly properties where it can (as the transforms lesson covered), so the final write is cheap — but the decision of what to write happens in JS, every frame, on the main thread, which a pure CSS/WAAPI animation never has to do at all.
A side-by-side comparison
| Dimension | CSS transition / @keyframes | Web Animations API (element.animate()) | GSAP |
|---|---|---|---|
| Runs on | Compositor thread, when eligible | Compositor thread, when eligible | Main thread (the ticker), always |
| Compositor-eligible | Yes, for transform/opacity | Yes, for transform/opacity | Yes for the final write, but the per-frame value is still computed in JS on the main thread |
| Complex sequencing | No — one effect, one timing function | Partial — one Animation per effect, manual coordination between them | Yes — timelines nest, label, overlap, and are themselves scrubbable as one unit |
| Custom/physics eases | No — bezier curves only, no overshoot | No — same bezier/keyframe limitation | Yes — elastic, bounce, and fully custom ease functions |
| Non-DOM targets | No | No | Yes — any object property, canvas state, WebGL uniform |
| Reading/decomposing existing transforms | No | No | Yes — matrix decomposition backs relative tweens like +=90 |
| Scroll-linking | Only via ScrollTimeline, no pinning/spacer management | ScrollTimeline gives the clock, not pinning or boundary callbacks | Yes — ScrollTrigger's full pin/snap/toggleActions apparatus |
| Imperative control | None natively | play/pause/currentTime/playbackRate/finished | Full control plus timeline-level scrubbing and dynamic re-targeting |
Practical guidance: choose by what the animation needs
Reach for CSS or WAAPI when the motion is simple, self-contained, and expressible as "from this state to that state" on transform and opacity — hovers, simple fades, a menu sliding in, anything where the compositor's narrow contract already covers everything you need. You get main-thread immunity for free and you haven't paid for a library to get it.
Reach for GSAP when you need orchestration across many elements, a non-standard ease, a non-DOM target, scroll-driven scrubbing with pinning, or genuine mid-flight dynamic control — the moment any of those show up, you're outside what CSS or WAAPI's model can express at all, not just outside what's convenient, and the main-thread cost is the price of admission for capabilities that have no compositor-only equivalent.
And the honest final point: these systems compose. It's entirely normal, and often the best answer, to use a plain CSS transition for a button's :hover state and reach for GSAP only for the choreographed hero-section timeline elsewhere on the same page. Nothing about using one obligates you to use it everywhere — the right call is per-animation, not per-project.
Closing the module
This module started from the smallest possible unit — a tween is just a start value, an end value, and a progress — and built outward through the ticker that drives it, the plugins that translate its plain numbers into CSS and SVG, the timelines that nest tweens inside other tweens, the eases that reshape progress before it's applied, the matrix math that lets independent transform components compose deterministically, and the scroll-driven clock that feeds ScrollTrigger the same normalized progress from an entirely different source. Every one of those pieces turned out to be a small, motivated extension of the same core idea rather than a separate feature bolted on beside it.
That core idea — a value with a start, an end, and a driven progress between them — isn't specific to GSAP. It's the same shape the animation module built from the browser's side: interpolation, easing, timing models, compositor delegation. GSAP and the browser's native animation systems are two different engines answering the same underlying question — "how do you get from one value to another smoothly" — and now you have the mechanism behind both of them, which is what makes the choice between them an engineering decision instead of a matter of preference.
Go deeper
- GSAP — why use GSAP over CSS/WAAPI — GSAP's own case for itself, worth reading against this lesson's more neutral comparison to see where the library's own framing agrees and where it oversells.
- MDN — Web Animations API — The full native engine referenced throughout this comparison, including ScrollTimeline and composite operations.
- web.dev — animations guide — Google's own practical guidance on choosing compositor-friendly CSS animations versus JavaScript-driven ones, covering much of the same ground as this lesson's table.
Check yourself
Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.
- Name two things a CSS cubic-bezier() easing curve fundamentally cannot express, and explain why GSAP's elastic/bounce eases aren't subject to the same limitation.
- Why is it imprecise to say 'GSAP is slower than CSS'? What's the more accurate description of where the cost actually shows up?
- A heavy synchronous JavaScript task blocks the main thread for 200ms. What happens to a running GSAP tween during that block, and what happens to a running compositor-eligible CSS transition? Why the difference?
- Give three capabilities from this module (timelines, eases, non-DOM targets, matrix decomposition, ScrollTrigger) that have no equivalent in plain CSS, and name the earlier lesson each one came from.
- What does WAAPI's ScrollTimeline give you that plain CSS lacks, and what does it still not give you that ScrollTrigger does?
- Explain the sense in which GSAP's final DOM write is 'still cheap' even though the animation as a whole runs on the main thread.
- Why does the lesson argue CSS and GSAP aren't competing alternatives so much as tools that compose on the same page? Give an example of using both together.