Animations
Animations enhance user experience by providing visual feedback, guiding attention, and creating delightful interactions. This guide provides practical examples and best practices for implementing animations in Next.js applications.
Smooth number transitions with NumberFlow for counters, statistics, and progress indicators. Perfect for dashboards and data visualizations.
import NumberFlow from '@number-flow/react';import { useState } from 'react';function AnimatedCounter() { const [count, setCount] = useState(0); return ( <div className="flex items-center gap-4"> <button onClick={() => setCount(c => c - 1)}>-</button> <NumberFlow value={count} locales="en-US" format={{ useGrouping: false }} className="text-2xl font-bold" willChange /> <button onClick={() => setCount(c => c + 1)}>+</button> </div> );}import NumberFlow from '@number-flow/react';import { useState } from 'react';function AnimatedCounter() { const [count, setCount] = useState(0); return ( <div className="flex items-center gap-4"> <button onClick={() => setCount(c => c - 1)}>-</button> <NumberFlow value={count} locales="en-US" format={{ useGrouping: false }} className="text-2xl font-bold" willChange /> <button onClick={() => setCount(c => c + 1)}>+</button> </div> );}Delightful icon animations using Motion (Framer Motion) for scale, rotation, and state changes. Add personality to your interface interactions.
import { motion, AnimatePresence } from 'motion/react';import { Heart, Copy, Check } from 'lucide-react';import { useState } from 'react';function AnimatedIcon() { const [isLiked, setIsLiked] = useState(false); const [isCopied, setIsCopied] = useState(false); return ( <div className="flex items-center gap-4"> <button onClick={() => setIsLiked(!isLiked)}> <motion.div animate={{ scale: isLiked ? [1, 1.2, 1] : 1, rotate: isLiked ? [0, -10, 10, 0] : 0, }} transition={{ duration: 0.3 }} > <Heart className={isLiked ? "fill-pink-500" : ""} /> </motion.div> </button> <button onClick={() => setIsCopied(true)}> <AnimatePresence mode="wait"> {isCopied ? ( <motion.div key="check" initial={{ scale: 0, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0, opacity: 0 }} > <Check /> </motion.div> ) : ( <motion.div key="copy" initial={{ scale: 0, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0, opacity: 0 }} > <Copy /> </motion.div> )} </AnimatePresence> </button> </div> );}import { motion, AnimatePresence } from 'motion/react';import { Heart, Copy, Check } from 'lucide-react';import { useState } from 'react';function AnimatedIcon() { const [isLiked, setIsLiked] = useState(false); const [isCopied, setIsCopied] = useState(false); return ( <div className="flex items-center gap-4"> <button onClick={() => setIsLiked(!isLiked)}> <motion.div animate={{ scale: isLiked ? [1, 1.2, 1] : 1, rotate: isLiked ? [0, -10, 10, 0] : 0, }} transition={{ duration: 0.3 }} > <Heart className={isLiked ? "fill-pink-500" : ""} /> </motion.div> </button> <button onClick={() => setIsCopied(true)}> <AnimatePresence mode="wait"> {isCopied ? ( <motion.div key="check" initial={{ scale: 0, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0, opacity: 0 }} > <Check /> </motion.div> ) : ( <motion.div key="copy" initial={{ scale: 0, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0, opacity: 0 }} > <Copy /> </motion.div> )} </AnimatePresence> </button> </div> );}Animate layout changes automatically when items are added, removed, or reordered. Perfect for dynamic lists and grids.
import { motion, AnimatePresence } from 'motion/react';function LayoutAnimation() { const [items, setItems] = useState([1, 2, 3]); const addItem = () => { setItems(prev => [...prev, prev.length + 1]); }; const removeItem = (id) => { setItems(prev => prev.filter(item => item !== id)); }; return ( <div> <button onClick={addItem}>Add Item</button> <motion.div layout className="space-y-2"> <AnimatePresence> {items.map(item => ( <motion.div key={item} layout initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.8 }} className="p-4 bg-muted rounded" > Item {item} <button onClick={() => removeItem(item)}>Remove</button> </motion.div> ))} </AnimatePresence> </motion.div> </div> );}import { motion, AnimatePresence } from 'motion/react';function LayoutAnimation() { const [items, setItems] = useState([1, 2, 3]); const addItem = () => { setItems(prev => [...prev, prev.length + 1]); }; const removeItem = (id) => { setItems(prev => prev.filter(item => item !== id)); }; return ( <div> <button onClick={addItem}>Add Item</button> <motion.div layout className="space-y-2"> <AnimatePresence> {items.map(item => ( <motion.div key={item} layout initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.8 }} className="p-4 bg-muted rounded" > Item {item} <button onClick={() => removeItem(item)}>Remove</button> </motion.div> ))} </AnimatePresence> </motion.div> </div> );}Lightweight CSS animations for hover effects, loading states, and transitions. No JavaScript required for simple animations.
/* Smooth transitions */.transition-smooth { transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);}/* Hover effects */.hover-lift { transition: transform 0.2s ease;}.hover-lift:hover { transform: translateY(-2px);}/* Loading animations */@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; }}.animate-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;}/* Stagger animations */.stagger-item { animation: fadeInUp 0.6s ease forwards; opacity: 0; transform: translateY(20px);}.stagger-item:nth-child(1) { animation-delay: 0.1s; }.stagger-item:nth-child(2) { animation-delay: 0.2s; }.stagger-item:nth-child(3) { animation-delay: 0.3s; }@keyframes fadeInUp { to { opacity: 1; transform: translateY(0); }}/* Smooth transitions */.transition-smooth { transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);}/* Hover effects */.hover-lift { transition: transform 0.2s ease;}.hover-lift:hover { transform: translateY(-2px);}/* Loading animations */@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; }}.animate-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;}/* Stagger animations */.stagger-item { animation: fadeInUp 0.6s ease forwards; opacity: 0; transform: translateY(20px);}.stagger-item:nth-child(1) { animation-delay: 0.1s; }.stagger-item:nth-child(2) { animation-delay: 0.2s; }.stagger-item:nth-child(3) { animation-delay: 0.3s; }@keyframes fadeInUp { to { opacity: 1; transform: translateY(0); }}Best Practices
Use transform and opacity for smooth 60fps animations
Avoid animating layout properties (width, height, top)
Use will-change sparingly and remove after animation
Test performance on lower-end devices
Keep animations under 300ms for micro-interactions
Use easing functions (ease-out, ease-in-out) for natural motion
Respect prefers-reduced-motion for accessibility
Provide meaningful feedback for user actions