Web Animations: CSS vs JavaScript, GSAP and Motion
Practice3 minInnovialabs editorial team
In short
Handle simple state changes (hover, reveal, expand) with CSS and complex sequences or scroll-driven scenes with GSAP or Motion; animate only transform and opacity, and turn motion down for prefers-reduced-motion.
The short answer
- CSS transitions and keyframes — for hover, focus, opening menus, revealing elements, looping loaders. Cheap, no library needed, and they work even before JavaScript loads.
- GSAP — for complex timelines, syncing many elements, scroll animations with pinned sections, SVG animation.
- Motion — for React interfaces: components entering and leaving, list reordering, gestures, state-to-state transitions. There is also a version for plain JavaScript.
The rule of thumb: if the animation is "from state A to state B on an event", use CSS. If you need sequences, time control, scroll linking or physics, use a JavaScript library.
Which properties animate cheaply
The browser renders a frame in stages: layout (computing sizes), paint (drawing pixels) and composite (combining layers). The earlier the stage an animation touches, the more expensive it is.
| Property | What gets recalculated | Cost |
|---|---|---|
transform, opacity | composite only | cheap |
color, background-color, box-shadow | paint | medium |
width, height, top, left, margin, padding | layout + paint | expensive |
Use transform: translateX() instead of left, and transform: scale() instead of width. To animate a shadow cheaply, fade in a pseudo-element that already has the shadow via opacity.
will-change: transform hints the browser to promote an element to its own layer. Use it sparingly and only on things that actually animate: extra layers cost memory.
CSS: when it is enough
.card {
transition: transform 200ms ease, opacity 200ms ease;
}
.card:hover {
transform: translateY(-4px);
}
@media (prefers-reduced-motion: reduce) {
.card { transition: none; }
.card:hover { transform: none; }
}
For scroll-based effects, modern browsers now offer scroll-driven animations (animation-timeline: view()) with no JavaScript. Support is still incomplete, so check it and make sure content stays visible when the animation does not run.
GSAP and Motion: scroll-triggered animation
A simple reveal on scroll can be done with Intersection Observer and a CSS class. If you need pinned sections, animation tied to scroll position or complex sequences, a library is more convenient.
GSAP with ScrollTrigger, respecting user preferences:
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
const mm = gsap.matchMedia();
mm.add("(prefers-reduced-motion: no-preference)", () => {
gsap.from(".feature", {
y: 40,
opacity: 0,
stagger: 0.1,
scrollTrigger: { trigger: ".features", start: "top 80%" }
});
});
Motion in React:
import { motion, MotionConfig } from "motion/react";
<MotionConfig reducedMotion="user">
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
/>
</MotionConfig>
reducedMotion="user" disables transform animations for people who turned on reduced motion in their system settings.
prefers-reduced-motion
For some people, on-screen motion causes dizziness and nausea. The prefers-reduced-motion media query tells you the user asked the system to reduce animation. What to do:
- remove parallax, large movements and auto-scrolling;
- keep short opacity and color changes — they are usually fine;
- make sure content is visible without animation and does not get stuck at
opacity: 0.
Avoiding jank
- Animate transform and opacity, not sizes and spacing.
- Do not interleave reading and writing layout (
offsetHeight, then a style write, then another read) — it forces synchronous layout. - Drive JavaScript animations with
requestAnimationFrameor a library, notsetInterval. - Do not attach heavy logic to the
scrollevent. - Test on a low-end phone and in the DevTools Performance panel with CPU throttling.
- Do not animate everything: one expressive animation beats ten small ones.