CSS, Web Animations or requestAnimationFrame?
The tool follows from the kind of motion, not taste. As a rule, the more declarative you stay, the better the browser can optimise it.
| Tool | Best for | Limitation |
|---|---|---|
transition | Moving between two states (hover, open/close) | Only a start and an end point |
@keyframes | Multi-step and looping motion (loaders, pulse) | Harder to control dynamically |
| Web Animations API | Start, pause and dynamic values from JavaScript | More code than CSS |
requestAnimationFrame | Custom physics, canvas, scroll-driven effects | You manage frames and cleanup manually |
For most business and shop sites, CSS plus a few lines of JavaScript to toggle a class is enough. Reach for a library such as GSAP only when you have complex timelines across many elements.
Why only transform and opacity?
For every frame the browser goes through layout, paint and composite. Changing width, top or margin repeats all three across the affected areas. transform and opacity, however, are normally handled in the composite stage alone, often on the GPU, without touching layout. The result is smoother motion and lower battery use.
/* Slow: triggers layout on every frame */
.card:hover { margin-top: -8px; }
/* Fast: composite-only */
.card { transition: transform 250ms ease, box-shadow 250ms ease; }
.card:hover { transform: translateY(-8px); }If part of your UI changes real layout (an accordion, say), a height change is unavoidable; keep it short and keep the surrounding elements simple. Building layouts is covered in our CSS Grid guide.

Timing and easing
Duration and curve affect the feeling of polish more than the effect itself. A few rules of thumb we use on our projects:
- Small feedback (hover, button press): 100 to 200 ms.
- Menus and modals opening or closing: 200 to 350 ms.
- Entering elements should use
ease-outand exiting onesease-in; reservelinearfor continuous motion such as a spinner. - Shorten animations above 500 ms for frequent user actions; users feel the delay.
A special note for bilingual sites: side-slide animations must be direction-aware. If a menu opens from the right, do not hard-code the translateX value; use a direction variable so the English (LTR) version works correctly too:
:root { --dir: 1; }
[dir="rtl"] { --dir: -1; }
.drawer { transform: translateX(calc(100% * var(--dir))); transition: transform 300ms ease-out; }
.drawer.open { transform: translateX(0); }The Web Animations API for JavaScript control
When you need to start, pause, reverse or drive an animation with dynamic values from code, element.animate() is the sweet spot between CSS and a manual loop: it has CSS-like performance and returns an Animation object with a finished promise. Support is broad in modern browsers; see the MDN documentation for details.
const toast = document.querySelector('.toast');
const anim = toast.animate(
[
{ opacity: 0, transform: 'translateY(16px)' },
{ opacity: 1, transform: 'translateY(0)' }
],
{ duration: 250, easing: 'ease-out', fill: 'forwards' }
);
// Run code only after the animation completes
anim.finished.then(() => setTimeout(() => toast.remove(), 3000));Reveal on scroll with IntersectionObserver
For a "reveal when it comes into view" effect, avoid scroll handlers; IntersectionObserver does it without loading the main thread, and CSS runs the animation:
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) {
e.target.classList.add('is-visible');
io.unobserve(e.target); // animate once
}
});
}, { threshold: 0.15 });
document.querySelectorAll('.reveal').forEach(el => io.observe(el));Accessibility: less motion for those who ask
For some people (vestibular disorders or migraine, for example) a lot of motion is unpleasant or even harmful. Operating systems offer a "reduce motion" setting and CSS exposes it via prefers-reduced-motion. Honouring it takes a few lines:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}Automatic, looping animations that last more than five seconds should also be pausable. Review the other related points in our web accessibility checklist.
Debugging and checklist
If an animation stutters, open the Performance tab in DevTools and look at the red frames; usually a heavy Layout or Paint sits in the middle of the motion. Enabling "Paint flashing" in the Rendering panel shows what gets repainted. Animations that shift the page during load can also hurt CLS; see the metrics in our Core Web Vitals guide.
- Are only
transformandopacitybeing animated? - Are duration and easing right for the type of motion?
- Is the motion direction correct in both RTL and LTR?
- Is
prefers-reduced-motionrespected? - Is
will-changetemporary and only on the needed element?
For animated, fast interfaces in your own project, get help from our web development service or browse our portfolio.
Frequently asked questions
Are CSS or JavaScript animations better?
For simple states like hover and open/close, CSS is simpler and well optimised. When you need programmatic control, pausing or dynamic values, the Web Animations API or a library fits better.
Why does my animation stutter on mobile?
Usually properties such as width, top or a heavy box-shadow are being animated, causing layout or paint on every frame. Switch to transform and opacity and inspect the Performance tab in DevTools.
How long should an animation last?
About 100 to 200 ms for small feedback and 200 to 350 ms for menus and modals is typically right. Longer motion on frequent actions makes the interface feel slow.
Do animations hurt SEO?
Not by themselves, but heavy JavaScript or layout shifts worsen speed and CLS, which can hurt rankings and user experience. Light, transform-based animation causes no trouble.

Comments
No comments yet. Be the first to share your thoughts.