I used to hate web animations. Every site seemed to have bouncing buttons, sliding panels, and spinning logos that served no purpose other than showing off. Then I discovered animations done right, and everything changed.
Good animations aren't decoration. They're communication. They guide users, provide feedback, and make interfaces feel responsive and alive. The difference between good and bad animation often comes down to understanding why you're animating, not just how.
The Purpose of Animation
Before adding any animation, ask yourself what problem it solves:
Feedback: Confirming user actions
Guidance: Drawing attention to important elements
Continuity: Showing relationships between interface states
Personality: Adding character without being distracting
If your animation doesn't serve one of these purposes, you probably don't need it.
CSS Animations: Start Here
CSS animations are performant and don't require JavaScript. They're perfect for simple interactions and micro-animations.
Button hover effects:
.button {
background: #007bff;
transition: all 0.2s ease;
transform: translateY(0);
}
.button:hover {
background: #0056b3;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
}
.button:active {
transform: translateY(0);
transition-duration: 0.1s;
}
Loading animations:
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid #f3f3f3;
border-top: 2px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
Slide-in content:
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: slideIn 0.3s ease-out;
}
JavaScript Animations: When You Need More Control
For complex animations or when you need to respond to user input, JavaScript gives you more flexibility.
Smooth scrolling to elements:
function scrollToElement(elementId) {
const element = document.getElementById(elementId);
const targetPosition = element.offsetTop;
const startPosition = window.pageYOffset;
const distance = targetPosition - startPosition;
const duration = 800;
let start = null;
function animation(currentTime) {
if (start === null) start = currentTime;
const timeElapsed = currentTime - start;
const run = easeInOutQuad(timeElapsed, startPosition, distance, duration);
window.scrollTo(0, run);
if (timeElapsed < duration) requestAnimationFrame(animation);
}
function easeInOutQuad(t, b, c, d) {
t /= d / 2;
if (t < 1) return c / 2 * t * t + b;
t--;
return -c / 2 * (t * (t - 2) - 1) + b;
}
requestAnimationFrame(animation);
}
Intersection Observer for scroll animations:
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-in');
observer.unobserve(entry.target);
}
});
}, observerOptions);
document.querySelectorAll('.animate-on-scroll').forEach(el => {
observer.observe(el);
});
Framer Motion: React Animations Made Easy
For React applications, Framer Motion provides a powerful yet simple API for complex animations.
Basic animations:
import { motion } from 'framer-motion';
function AnimatedCard({ children }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{children}
</motion.div>
);
}
Page transitions:
import { AnimatePresence, motion } from 'framer-motion';
function PageTransition({ children, location }) {
return (
<AnimatePresence mode="wait">
<motion.div
key={location.pathname}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
</AnimatePresence>
);
}
Staggered animations:
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1
}
}
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
};
function StaggeredList({ items }) {
return (
<motion.ul variants={container} initial="hidden" animate="show">
{items.map((item, index) => (
<motion.li key={index} variants={item}>
{item}
</motion.li>
))}
</motion.ul>
);
}
GSAP: Professional-Grade Animations
For complex timelines and advanced effects, GSAP is the industry standard.
Timeline animations:
import { gsap } from 'gsap';
function animateHero() {
const tl = gsap.timeline();
tl.from('.hero-title', {
duration: 0.8,
y: 50,
opacity: 0,
ease: 'power2.out'
})
.from('.hero-subtitle', {
duration: 0.6,
y: 30,
opacity: 0,
ease: 'power2.out'
}, '-=0.4')
.from('.hero-button', {
duration: 0.4,
scale: 0,
ease: 'back.out(1.7)'
}, '-=0.2');
}
Scroll-triggered animations:
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
gsap.utils.toArray('.reveal').forEach(element => {
gsap.fromTo(element,
{ opacity: 0, y: 50 },
{
opacity: 1,
y: 0,
duration: 0.8,
scrollTrigger: {
trigger: element,
start: 'top 80%',
end: 'bottom 20%',
toggleActions: 'play none none reverse'
}
}
);
});
Performance Considerations
Animations can hurt performance if not implemented carefully.
Use transform and opacity for smooth animations:
/* Good: Uses GPU acceleration */
.element {
transform: translateX(100px);
opacity: 0.5;
}
/* Bad: Causes layout recalculation */
.element {
left: 100px;
width: 200px;
}
Optimize with will-change:
.animated-element {
will-change: transform, opacity;
}
/* Remove after animation completes */
.animated-element.animation-complete {
will-change: auto;
}
Use requestAnimationFrame for JavaScript animations:
function animate() {
// Animation logic here
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
Accessibility and Reduced Motion
Always respect user preferences for reduced motion:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
// Check for reduced motion preference
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
if (!prefersReducedMotion.matches) {
// Only animate if user hasn't requested reduced motion
element.animate(keyframes, options);
}
Animation Principles
Good animations follow these principles:
Timing: Fast enough to feel responsive (under 300ms for most interactions), slow enough to be perceived
Easing: Use natural easing curves. ease-out for entrances, ease-in for exits, ease-in-out for transitions
Consistency: Establish animation patterns and stick to them throughout your interface
Restraint: Less is more. A few well-placed animations are better than constant motion
Common Mistakes to Avoid
Animating too many things at once: Focus attention, don't scatter it
Making animations too slow: Anything over 500ms feels sluggish for UI interactions
Ignoring the animation's purpose: Every animation should have a clear reason for existing
Forgetting mobile performance: Test animations on actual devices, not just desktop
Building an Animation System
Create reusable animation utilities:
/* Animation utilities */
.fade-in { animation: fadeIn 0.3s ease-out; }
.slide-up { animation: slideUp 0.4s ease-out; }
.bounce-in { animation: bounceIn 0.5s ease-out; }
/* State classes */
.is-loading { animation: pulse 1.5s ease-in-out infinite; }
.is-error { animation: shake 0.5s ease-in-out; }
.is-success { animation: checkmark 0.6s ease-out; }
The best animations are the ones users don't consciously notice. They just make the interface feel more responsive, more alive, and more pleasant to use. Focus on enhancing the user experience, not showing off your animation skills.