How to Choose Between CSS, GSAP, Motion and Three.js for Website Animation
How to Choose Between CSS, GSAP, Motion and Three.js for Website Animation A button changes color on hover. A card expands into a modal. A product story pins sections while images move through a sequence. A liquid surf
How to Choose Between CSS, GSAP, Motion and Three.js for Website Animation
A button changes color on hover. A card expands into a modal. A product story pins sections while images move through a sequence. A liquid surface reacts to the pointer.
Calling all four of these things βwebsite animationβ hides the decision that actually matters.
CSS, GSAP, Motion and Three.js can all participate in a website's motion system, but they operate at different layers. CSS is excellent when animation belongs to styling and interface state. Motion fits naturally when movement follows React state, layout or gestures. GSAP becomes attractive when timing and orchestration are the difficult parts. Three.js belongs to a different rendering model entirely: scenes, cameras, meshes, materials and shaders rather than ordinary DOM elements.
The useful question is therefore:
What kind of thing am I animating, and what is making the animation difficult?
That question usually narrows the choice quickly.
The short version
| Choose | When the problem is mainly |
|---|---|
| CSS | States, hover/focus feedback, simple entrances and self-contained transitions |
| Motion | React state, component enter/exit, layout changes, gestures and UI-oriented motion |
| GSAP | Precise sequences, scroll choreography, timelines, SVG or imperative multi-element animation |
| Three.js | WebGL scenes, shaders, particles, 3D objects and graphics that should not be DOM elements |
This is not a ladder from βbasicβ to βadvanced.β
A well-built marketing page might use all four. Its buttons could use CSS, its mobile navigation could use Motion, its scroll narrative could use GSAP and its hero canvas could use Three.js.
The architecture should follow the interaction rather than forcing every effect through whichever package happens to be installed already.
Start with CSS more often than you think
Suppose a button needs to move upward by two pixels and change background color on hover.
Installing an animation library for that interaction does not make it more sophisticated. It gives a small state transition another runtime abstraction.
.button {
transform: translateY(0);
transition:
transform 180ms ease,
background-color 180ms ease;
}
.button:hover {
transform: translateY(-2px);
}
CSS transitions are especially good when animation is a direct consequence of CSS state:
:hover:focus-visible:active- a class changing
- a data attribute changing
- a media query changing
Keyframe animations extend that model when the movement has intermediate states rather than a simple start and end.
@keyframes reveal {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
Browsers already have substantial machinery for running these animations. The cost depends heavily on which properties are changing, not simply whether CSS or JavaScript initiated them.
That distinction matters.
βCSS is fast, JavaScript is slowβ is too crude to be useful. Animating transform and opacity is very different from repeatedly forcing layout and paint work. A badly chosen CSS property can still produce a bad animation.
The native boundary keeps moving
Before reaching for a library, check what the platform can already express.
CSS scroll-driven animations can tie keyframe progress directly to a scroll or view timeline. The View Transitions API handles another class of state and page transitions natively, while the Web Animations API gives JavaScript direct control over browser animations without requiring a full animation library.
These APIs do not make Motion or GSAP obsolete. They move the point at which a dependency becomes justified.
A progress indicator linked directly to document scroll may no longer require scroll-event code. A tightly choreographed pinned product sequence with overlapping states still has a very different coordination problem.
Use the smallest mechanism that accurately describes the interaction.
CSS starts becoming awkward when state is no longer enough
Imagine a hero sequence with six elements:
- the eyebrow appears;
- the heading reveals;
- two decorative lines extend;
- the image begins moving before the heading finishes;
- metadata follows 120ms later;
- everything reverses differently when the section leaves.
You can encode that with delays and keyframes.
The uncomfortable part is maintaining the relationships.
Change the duration of the heading and you may need to recalculate several delays. Add another step and the sequence becomes a small scheduling system expressed through stylesheets.
Once the timing relationships become harder to maintain than the transforms themselves, a timeline starts earning its place.
Choose Motion when animation belongs to React state
Consider an accordion.
The user clicks a control, React state changes, the component changes size, surrounding content moves, an icon rotates and perhaps some content enters or exits.
This problem is mainly a relationship between component state and visual state.
That is where Motion tends to feel natural.
import { motion } from "motion/react";
function Panel({ open }) {
return (
<motion.div
animate={{
opacity: open ? 1 : 0.6,
scale: open ? 1 : 0.98,
}}
/>
);
}
Motion provides React-oriented APIs for animation, gestures, scrolling and layout changes. Its layout system can animate changes in an element's position and size, while layoutId can connect visually related elements across states.
That makes it particularly useful for interactions such as:
- accordions;
- tabs;
- modals;
- drawers;
- expanding cards;
- sortable interfaces;
- shared-element transitions;
- components entering and leaving the tree;
- drag interactions;
- hover and tap behaviour tied to component state.
For React applications, the important advantage is conceptual.
Your animation often remains close to the state that caused it.
You are not asking an external timeline to observe that isOpen became true; the animated state can be expressed directly in the component.
Motion is particularly useful around layout
Layout animation is one of those jobs that sounds simple until you attempt it manually.
A card changes position because another card disappeared. The browser immediately knows the new layout, but visually connecting the previous geometry to the next geometry takes more work than adding a CSS transition to left and top.
Motion provides layout-specific APIs for that class of problem, including transform-based layout animation.
If your interface contains lots of movement caused by application state rather than authored timelines, this can remove a considerable amount of coordination code.
Motion can also handle scroll, sequences and gestures
Motion is not confined to small component transitions.
It supports scroll-triggered and scroll-linked animation, gestures such as hover, tap and drag, and animation sequences through its broader APIs.
This overlap is worth acknowledging because the boundaries between tools are not absolute. The question is which model most naturally describes the interaction.
If the animation reads as this component is now in this state, Motion usually gives you a vocabulary that fits the application.
Choose GSAP when choreography becomes the problem
Now consider a landing-page hero where:
- text is split into lines;
- lines reveal in sequence;
- an image scales simultaneously;
- a mask begins halfway through the image animation;
- a decorative SVG path follows;
- the sequence can be scrubbed or reversed;
- a later section pins while several animations respond to scrolling.
At this point, the interesting object is the sequence itself.
GSAP is very good at making that sequence explicit.
const tl = gsap.timeline();
tl.from(".title", {
yPercent: 100,
duration: 0.8,
})
.from(
".image",
{
scale: 1.15,
duration: 1.2,
},
"<0.2"
)
.from(
".meta",
{
opacity: 0,
y: 12,
},
"-=0.4"
);
The exact syntax matters less than what it represents: relationships between animations are first-class.
GSAP's core exposes tweens and timelines, while its wider ecosystem covers scroll animation, SVG work, text, dragging, FLIP-style transitions and other interaction problems.
This is particularly comfortable when a designer is thinking in terms such as:
Start B shortly after A, let C overlap both, then reverse the sequence differently.
Trying to express that as a collection of independent component states can be more awkward than acknowledging that you have a timeline.
GSAP and React solve different layers
Using GSAP in React is not unusual, but the architectural relationship differs from Motion.
Motion extends React's declarative component model. GSAP is framework-agnostic and generally operates imperatively on animation targets.
In a React component, lifecycle deserves attention.
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
gsap.registerPlugin(useGSAP);
export function Hero() {
const root = useRef(null);
useGSAP(
() => {
gsap.from(".hero-title", {
y: 40,
opacity: 0,
});
},
{ scope: root }
);
return (
<section ref={root}>
<h1 className="hero-title">Hello</h1>
</section>
);
}
GSAP recommends registering useGSAP, and the hook uses GSAP context to help scope animation work and clean it up with the component lifecycle.
In Next.js App Router projects, code using hooks and browser-side animation belongs on the client side, which is why the example includes "use client".
That boundary is easy to overlook in animation demos.
A demo has one mount.
An application has route changes, remounts, responsive conditions, state updates and development behaviour such as React Strict Mode. Animations that create listeners, triggers or imperative state need to disappear as carefully as they appeared.
Scroll-heavy work is a common reason to choose GSAP
ScrollTrigger changes the calculation for experiences where scroll itself becomes part of the choreography.
A simple fade when an element enters the viewport does not require that machinery. CSS, IntersectionObserver, Motion or another small mechanism may cover it cleanly.
A pinned sequence where five visual states overlap, scrub, reverse and respond to viewport changes is a different problem. There, combining scroll state with GSAP's timeline model can make the implementation easier to reason about.
Choosing GSAP because one heading needs to fade in is usually excessive. Choosing it because the page contains several tightly coordinated scroll sequences is much easier to justify.
Choose Three.js when the browser needs to render a scene
Three.js changes the comparison because it is primarily a rendering library, not another DOM animation abstraction.
CSS, Motion and GSAP commonly animate things the browser already rendered as HTML or SVG.
Three.js usually renders the visual itself.
You create objects, add them to a scene, position a camera and render the result through WebGL.
A minimal structure looks more like this:
import * as THREE from "three";
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
100
);
camera.position.z = 5;
const renderer = new THREE.WebGLRenderer({
antialias: true,
});
renderer.setSize(window.innerWidth, window.innerHeight);
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial();
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
renderer.setAnimationLoop(() => {
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
});
Three.js recommends defining the animation loop with renderer.setAnimationLoop() rather than manually wiring requestAnimationFrame() because the renderer can manage the loop appropriately across supported rendering contexts.
This changes the engineering problem.
A DOM animation asks questions such as:
Which element should move, and when?
A Three.js implementation adds another set:
What should be rendered? What geometry and materials are involved? What resolution is the canvas? Which textures are loaded? Does the scene need another frame when nothing has changed?
Three.js makes sense for genuinely graphical problems
Examples include:
- 3D product viewers;
- particle fields;
- shader-driven backgrounds;
- fluid or distortion effects;
- spatial scenes;
- 3D typography;
- image transitions implemented through textures;
- surfaces that need per-pixel graphical effects.
A collection of pricing cards moving upward does not become better because each card is a plane inside WebGL.
Keeping semantic content in the DOM usually gives you simpler text rendering, accessibility, responsiveness and interaction behaviour.
Use Three.js when the visual idea actually benefits from graphics rendering.
GSAP, Motion and Three.js can work at different layers
βGSAP vs Three.jsβ often turns out to be the wrong comparison.
GSAP can animate numeric properties belonging to Three.js objects while Three.js remains responsible for rendering the scene.
gsap.to(mesh.rotation, {
y: Math.PI * 2,
duration: 2,
ease: "power3.inOut",
});
Three.js renders the pixels.
GSAP controls how the values evolve over time.
That division of labour is useful when a graphical scene still needs authored choreography.
Motion can also participate in Three.js animation. Its current Three.js integration can animate objects, materials and shader uniforms, including alongside DOM animation sequences. Direct Three.js integration is part of Motion's Motion+ offering, so it should not be confused with the ordinary motion/react component workflow used earlier in this article.
The larger point remains the same: these tools are not four mutually exclusive choices.
They occupy overlapping layers.
A practical decision tree
Before installing anything, ask what drives the movement.
Is it mostly a style state?
Hover, focus, active state, a simple reveal or a contained decorative keyframe usually belongs in CSS.
Check native APIs before adding another dependency, particularly for straightforward scroll-linked or transition work.
Does movement closely follow React component state?
A modal mounting, an accordion changing height, cards reordering or a drag interaction points naturally toward Motion.
The component tree and visual state can remain closely connected.
Has timing become an authored system?
Once the implementation depends on overlapping sequences, coordinated SVG movement, multiple timelines or detailed scroll choreography, GSAP becomes easier to justify.
The timeline is now part of the design.
Does the effect require a graphics renderer?
Shaders, particle systems, 3D scenes and texture-based effects belong in Three.js territory.
Another animation system can still control values inside that scene.
Do not choose by demo smoothness
Animation demos run under suspiciously pleasant conditions.
There may be one effect on the page, no analytics, no application state, no route transition, no CMS content with unpredictable lengths and no low-powered phone trying to render the whole thing.
Production introduces different questions.
What properties are changing?
Performance depends on the work an animation creates.
A small animation library does not rescue an effect that repeatedly causes expensive layout. Nor is a CSS animation automatically cheap simply because JavaScript is absent.
Transforms and opacity often have a very different rendering profile from properties that repeatedly affect layout or expensive paint work.
Measure the actual page rather than treating the choice of API as a performance benchmark.
Does anything run continuously?
A button transition ends after 180ms.
Many animated Three.js scenes render continuously while active. Others can render only when something changes.
Those models have radically different budgets.
For a continuously rendered scene, ask whether it needs to remain active while offscreen, whether the tab is visible, what device-pixel ratio is being used, how expensive the shaders are, how large the textures are and whether post-processing is justified.
A smooth desktop demo does not answer those questions.
What happens on touch?
A magnetic cursor interaction built around precise mouse coordinates does not become mobile-friendly because its dimensions respond to the viewport.
Touch has a different input model.
Hover-only ideas need an alternate interaction or should disappear when they no longer have a job.
What happens with reduced motion?
prefers-reduced-motion should influence the interaction design, not merely reduce every duration from 0.8 to 0.4.
Sometimes the correct reduced-motion state is a simpler transition.
Sometimes it is an immediate state change.
Sometimes a decorative graphical scene should become a static frame.
Reduced motion is also only one part of accessibility. If animation belongs to a modal, menu, tabs or another interactive component, the static and animated states still need appropriate semantics, keyboard behaviour, focus handling and visible focus. Motion should not become responsible for hiding an interaction model that does not work without it.
Standardize responsibilities before APIs
Frontend teams like standardization because every additional dependency introduces another API, another upgrade path and another thing somebody needs to understand.
That instinct is healthy until a single animation tool is forced into jobs that another layer already handles better.
A sensible project might divide responsibility like this:
CSS
βββ hover/focus feedback
βββ simple transitions
βββ low-level visual states
Motion
βββ component enter/exit
βββ layout changes
βββ gestures
βββ state-driven UI motion
GSAP
βββ hero timeline
βββ scroll storytelling
βββ complex SVG sequence
Three.js
βββ interactive WebGL hero
The useful standard is not βall animation uses Library X.β
It is knowing which layer owns which kind of movement.
Where source-first animation patterns can help
There is also a choice before the library choice: whether the interaction should be built from zero at all.
A complicated cursor, text treatment, page transition or WebGL effect often contains more production decisions than its visual idea suggests. Resize handling, pointer behaviour, cleanup, breakpoints, reduced-motion states and rendering constraints can surround the relatively small piece of code that creates the visible effect.
Hyperiux Vault takes a source-first approach to this problem for React and Next.js projects. Interaction patterns are added as editable source, so the implementation can be inspected and adapted inside the project rather than treated as a black-box animation runtime.
That matters most when the starting point is close to what you need but production conditions are different. The breakpoint may need to move. GSAP timing may need to match an existing motion system. A Three.js scene may need a cheaper mobile path. A cursor effect may need to disappear on touch.
The animation engine remains whichever engine makes sense for the effect. Vault's role is different: you begin with the interaction already expressed in code, then change the implementation where the project requires it.
If that workflow fits the project, browse Hyperiux Vault's interaction patterns and inspect the source approach before rebuilding the effect from a blank component.
A practical rule for each tool
If you need a rule you can remember during implementation:
Use CSS while the animation is primarily a styling or browser-state problem.
Use Motion when movement primarily follows React state, layout and gestures.
Use GSAP when timing, sequencing and choreography have become the system you need to manage.
Use Three.js when the visual needs a graphics renderer rather than another DOM animation API.
There will be exceptions. That is fine.
The mistake is treating animation complexity as a prestige scale where CSS is the beginner option and WebGL is the advanced one.
A two-property CSS transition can be exactly the right implementation. A shader-powered hero can also be exactly right.
The quality of the decision comes from matching the mechanism to the interaction.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.