CSS Animations vs JavaScript Animations
Answer
Both CSS and JavaScript can create animations, but they have different performance characteristics, capabilities, and use cases. Understanding when to use each is crucial for smooth, performant animations.
Comparison Overview
CSS Animations
/* CSS Transitions - state changes */
.button {
background: blue;
transform: scale(1);
transition: transform 0.3s ease, background 0.3s ease;
}
.button:hover {
background: darkblue;
transform: scale(1.05);
}
/* CSS Keyframe Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.5s ease forwards;
}
/* Complex keyframe animation */
@keyframes pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
.pulsing {
animation: pulse 2s ease-in-out infinite;
}
JavaScript Animations
// requestAnimationFrame - most control
function animate(element, startX, endX, duration) {
const startTime = performance.now();
function frame(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Easing function
const easeOut = 1 - Math.pow(1 - progress, 3);
const currentX = startX + (endX - startX) * easeOut;
element.style.transform = `translateX(${currentX}px)`;
if (progress < 1) {
requestAnimationFrame(frame);
}
}
requestAnimationFrame(frame);
}
// Web Animations API (WAAPI)
element.animate(
[
{ transform: "translateX(0)", opacity: 1 },
{ transform: "translateX(100px)", opacity: 0 },
],
{
duration: 500,
easing: "ease-out",
fill: "forwards",
}
);
// WAAPI with control
const animation = element.animate(keyframes, options);
animation.pause();
animation.play();
animation.reverse();
animation.playbackRate = 0.5; // Half speed
animation.finished.then(() => console.log("Done!"));
Performance Comparison
Properties for Smooth Animation
/* ✅ GPU-accelerated (compositor thread) */
.smooth-animation {
/* Only these properties are truly GPU-accelerated */
transform: translateX(100px);
opacity: 0.5;
}
/* ❌ Triggers layout/paint (main thread) */
.janky-animation {
/* Avoid animating these */
width: 200px;
height: 100px;
top: 50px;
left: 50px;
margin: 20px;
padding: 10px;
border-width: 2px;
font-size: 16px;
}
/* ✅ Use transform instead of position */
.move-element {
/* Instead of: left: 100px */
transform: translateX(100px);
/* Instead of: width/height changes */
transform: scale(1.5);
}
Use Cases
| Animation Type | Recommended Approach |
|---|---|
| Hover effects | CSS transitions |
| Loading spinners | CSS keyframes |
| Page transitions | CSS or WAAPI |
| Scroll-linked | JavaScript (Intersection Observer + WAAPI) |
| Complex sequences | JavaScript (WAAPI or GSAP) |
| Physics-based | JavaScript |
| Interactive/game | JavaScript |
| User gesture driven | JavaScript |
Animation Libraries
// GSAP (GreenSock) - powerful animation library
gsap.to(".box", {
x: 100,
rotation: 360,
duration: 1,
ease: "power2.out",
});
// Timeline for sequences
const tl = gsap.timeline();
tl.to(".header", { y: -100, duration: 0.5 })
.to(".content", { opacity: 1, duration: 0.3 })
.to(".footer", { y: 0, duration: 0.5 });
// Framer Motion (React)
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
Content
</motion.div>;
Scroll-Linked Animations
// Modern CSS scroll-driven animations
@keyframes reveal {
from { opacity: 0; transform: translateY(50px); }
to { opacity: 1; transform: translateY(0); }
}
.scroll-reveal {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% cover 40%;
}
// JavaScript approach with Intersection Observer
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.animate([
{ opacity: 0, transform: 'translateY(50px)' },
{ opacity: 1, transform: 'translateY(0)' }
], {
duration: 500,
fill: 'forwards'
});
observer.unobserve(entry.target);
}
});
}, { threshold: 0.1 });
document.querySelectorAll('.animate-on-scroll')
.forEach(el => observer.observe(el));
Best Practices
/* 1. Use will-change sparingly */
.will-animate {
will-change: transform, opacity;
}
/* 2. Prefer transform and opacity */
.performant {
transform: translateX(100px) scale(1.1);
opacity: 0.8;
}
/* 3. Use contain for complex lists */
.list-item {
contain: layout style paint;
}
/* 4. Reduce motion for accessibility */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
// Check for reduced motion preference
const prefersReducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
if (!prefersReducedMotion) {
// Run animations
}
Decision Flowchart
Key Points
- CSS: Simple, declarative, auto-optimized
- JavaScript: Complex, interactive, more control
- Animate
transformandopacityfor performance - Web Animations API bridges CSS and JS
- Use
prefers-reduced-motionfor accessibility - Libraries like GSAP for complex animations
- Measure with DevTools Performance tab