Animation can make a website feel alive or feel broken, depending on how you do it. This article covers the three tools you have — CSS transitions, CSS keyframes, and JavaScript — when to reach for each, when to do nothing, and the patterns that make motion feel intentional rather than gratuitous.
What animation is for
Good animation does three things. It provides feedback: this button is interactive, that just changed. It guides attention: the new message is over here. It conveys relationship: this is the expanded version of that. Bad animation is decoration without purpose — bouncing buttons, spinning loaders that take too long, parallax that distracts from the content.
The principle: every animation should earn its place. If it does not help the user understand what just happened or what to do next, leave it out.
CSS transitions: the default tool
CSS transitions animate between two states. You declare the starting state, the ending state, and which properties should animate:
.button {
background: #ff8a3d;
transition: background 0.2s ease;
}
.button:hover {
background: #ff3d8a;
}
When the user hovers, the background smoothly fades from orange to pink over 0.2 seconds. Use transitions for:
- Hover and focus states.
- Toggling visibility (with care).
- Small changes in size, colour, or position.
The transition shorthand is transition: property duration easing. You can transition multiple properties at once: transition: all 0.3s ease (the lazy version) or be specific.
CSS keyframes: the bigger tool
For animations that go through multiple stages — bounce, pulse, spin, slide-in — use @keyframes:
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.loader {
animation: spin 1s linear infinite;
}
Keyframes let you declare each stage explicitly. The animation runs forever (infinite) at one revolution per second. Common uses: loading spinners, attention-grabbing pulses, multi-stage reveals.
The transform and opacity rule
When animating, prefer transform and opacity over properties like width, height, top, or left. The reason is performance: transform and opacity can be handled by the GPU and run on a separate thread, while layout-triggering properties force the browser to recalculate every frame.
Instead of animating width from 0 to 300px, animate transform: scaleX(0) to transform: scaleX(1). The visual result is the same, the performance is dramatically better.
When to use JavaScript
CSS can handle a surprising amount, but reach for JavaScript when:
- The animation must respond to user interaction (drag, scroll position, mouse position).
- You need to chain animations based on state (after one finishes, start another).
- You want physics-based motion (springs, momentum, easing curves).
- You are animating something the browser cannot natively transition (SVG paths, complex layouts).
For these cases, the Web Animations API is the modern tool:
const el = document.querySelector(".box");
el.animate(
[{ transform: "translateX(0)" }, { transform: "translateX(300px)" }],
{ duration: 500, fill: "forwards", easing: "cubic-bezier(.2,.8,.2,1)" }
);
It is more powerful than CSS transitions and runs at the same speed. For physics-based motion, libraries like GSAP or Framer Motion (for React) are excellent.
Easing: the difference between good and great
Easing curves are how motion feels. The default ease is fine. The custom curves make motion feel professional:
cubic-bezier(.2,.8,.2,1)— a classic ease-out. Things arriving decelerate.cubic-bezier(.4,0,.2,1)— Material Design's standard easing.linear— only for things that loop (spinners). Never for entry animations.
Most professional motion uses ease-out for things appearing and ease-in for things disappearing. The asymmetry feels natural: objects in the real world decelerate as they arrive, accelerate as they leave.
Respecting user preferences
Some users experience motion sickness from animations. Respect them with the prefers-reduced-motion media query:
.button {
transition: transform 0.2s ease;
}
@media (prefers-reduced-motion: reduce) {
.button {
transition: none;
}
}
Reduce or remove all non-essential animation when the user has expressed the preference at the OS level. This is a small gesture that costs almost nothing and makes the web more usable for a meaningful chunk of people.
Common pitfalls
- Animating layout properties. Use
transformandopacityinstead ofwidth,height,top,left. - Too many concurrent animations. Each animated element costs CPU. If your page has 50 spinners, performance suffers.
- Slow transitions. Anything over 400ms feels sluggish for UI feedback. Save long durations for storytelling moments.
- Animating for the sake of it. If the animation does not help the user, leave it out.
- Ignoring
prefers-reduced-motion. Respect the user's preference.
The FLIP technique for layout animation
The hardest thing to animate in CSS is layout changes — a list reordering, an accordion expanding, a card moving from one position to another. CSS cannot smoothly transition between the old and new layouts because there is no intermediate state. The FLIP technique solves this with a small amount of JavaScript:
function flip(element, newPosition) {
const oldRect = element.getBoundingClientRect();
// apply the change
element.style.transform = `translate(${newRect.left - oldRect.left}px, ${newRect.top - oldRect.top}px)`;
element.style.transition = "transform 0s";
// play the inverse animation
requestAnimationFrame(() => {
element.style.transition = "transform 0.3s ease";
element.style.transform = "";
});
}
First, Last, Invert, Play. You measure the old position, apply the change, measure the new position, set a transform that puts the element visually back where it was, then animate the transform back to zero. The browser smoothly tweens from the old to the new layout without ever doing expensive layout calculations.
Animating with requestAnimationFrame
For very low-level control, requestAnimationFrame lets you write your own animation loop, the way Three.js does. The pattern:
function loop(time) {
const t = time / 1000;
element.style.transform = `translateX(${Math.sin(t) * 100}px)`;
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
The browser calls your function with the current timestamp. You compute the new state, apply it, and ask to be called again. This is more code than CSS or the Web Animations API, but it gives you total control. Reach for it only when nothing else fits.
SVG animation
SVG elements are part of the DOM, so they animate with the same tools as HTML. CSS transitions and keyframes work, plus SVG has its own attributes that animate well: stroke-dashoffset for the "drawing" effect, cx/cy for circle movement, r for circle radius:
.path {
stroke-dasharray: 100;
stroke-dashoffset: 100;
animation: draw 2s forwards;
}
@keyframes draw {
to { stroke-dashoffset: 0; }
}
The MDN SVG attribute reference has the complete list. For complex SVG animation (morphing paths, coordinated sequences), GSAP's MorphSVG plugin is the standard tool.
Sequenced animations: choreography
For multi-step animations — a button that pulses, then expands, then reveals a panel — you need to coordinate timing. Three approaches:
- CSS animation-delay — set staggered delays on multiple elements.
- animationend events — trigger the next animation when the previous one finishes.
- Web Animations API — chain animations using
animation.finishedpromises.
For complex sequences, the Web Animations API gives you the cleanest code:
async function reveal() {
await box.animate(fadeIn, { duration: 300 }).finished;
await panel.animate(slideDown, { duration: 300 }).finished;
await content.animate(fadeIn, { duration: 300 }).finished;
}
Read it like a recipe. Each step awaits the previous one. Awaiting a finished animation is one of the most underrated features of the Web Animations API.
Further reading
Animation has matured into a discipline of its own. These are the sources the Mangobaz team returns to.
- MDN: CSS Animations — The reference we trust for keyframes, timing functions, and animation events.
- Motion (formerly Framer Motion) — The library we reach for when CSS transitions are not expressive enough.
- W3C WAI: Animations — Accessibility guidance for motion — covers prefers-reduced-motion and vestibular safety.
FAQ
CSS or JavaScript animation?
CSS for declarative state changes (hover, focus, simple entrances). JavaScript for interaction-driven, sequenced, or physics-based motion. Most production code uses both.
What is the Web Animations API?
A browser API that gives JavaScript programmatic access to the same animation engine CSS uses. It is faster than animating with setInterval or requestAnimationFrame and provides better timing control. Use it instead of older libraries for simple cases.
How do I animate an SVG path?
Use CSS to animate stroke-dashoffset to create the "drawing" effect. Set stroke-dasharray to the path length and animate stroke-dashoffset from that length to 0. For complex SVG animation, the GSAP library has dedicated plugins.
What is FLIP?
First, Last, Invert, Play. A technique for animating layout changes. Record the starting position, apply the change, record the ending position, apply an inverse transform, then animate the inverse to zero. Useful for animating list reorders, accordion expansions, and other layout transitions.
How do I make a loading spinner?
Either a CSS spinner (a rotating border) or an SVG with a rotating element. Avoid spinners entirely if you can show real progress. See our JS intermediate article for fetch + loading patterns.
Should I animate with React/Vue?
Use the built-in <Transition> component in Vue or the <AnimatePresence> wrapper in Framer Motion. They handle enter/exit animations declaratively. For simple cases, plain CSS transitions work fine without any framework help.
What is the best easing function?
For entry animations, use an ease-out curve like cubic-bezier(.2,.8,.2,1). For exit animations, use ease-in. For interactive transitions, use ease-in-out. Avoid linear except for things that loop continuously (spinners, progress indicators).
How long should an animation take?
UI feedback: 100-200ms. State changes (toggling a panel): 200-300ms. Page transitions: 300-500ms. Anything over 500ms feels slow unless it is storytelling content (a hero animation, an onboarding sequence). The faster the animation, the more "snappy" the interface feels.
Once you have shipped animations that respect user preferences and follow the performance rules, you have the foundation for any motion design challenge. For the surrounding CSS toolkit, see our Custom Properties article.
Test every animation thoroughly before deploying, and respect user preferences always.
Run a final pass to ensure every animation is performant, accessible, and respects user preferences.
Motion is a powerful tool, but like all powerful tools it deserves respect. The animations you ship should always serve the user, never just the designer.
Take the time to learn each technique deeply — the magic is in the details.
Take the time to learn each technique thoroughly — the real magic is in the small details you discover along the way.
Homework
Animate a small UI element using each of the three tools:
- Use a CSS transition on a button's hover state.
- Use a CSS keyframe animation for a loading spinner.
- Use the Web Animations API to slide a panel in and out when a button is clicked.
- Wrap all of them in a
prefers-reduced-motionguard.
Bonus: build a small accordion using FLIP for smooth height animation. When you can confidently choose between CSS and JavaScript animation, you have the foundation for any interactive UI. For more on the surrounding CSS, see our Custom Properties article for theming animated components.