Animation Recipes
Copy-paste animation snippets using Framer Motion for common UI patterns
Animation Recipes
Quick copy-paste animation snippets for common UI patterns.
Basic Animations
Fade In
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.5 }}>
Content
</motion.div>Slide Up
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
Content
</motion.div>Scale In
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3 }}
>
Content
</motion.div>Button Animations
Hover Scale
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
Click me
</motion.button>Lift on Hover
<motion.button
whileHover={{ y: -2, boxShadow: "0 4px 12px rgba(0,0,0,0.15)" }}
whileTap={{ y: 0 }}
transition={{ duration: 0.2 }}
>
Hover me
</motion.button>Ripple Effect
<motion.button
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<motion.div
initial={{ scale: 0, opacity: 1 }}
animate={{ scale: 1.5, opacity: 0 }}
transition={{ duration: 0.5 }}
/>
Tap me
</motion.button>Card Animations
Hover Card
<motion.div
className="card"
whileHover={{
y: -8,
boxShadow: "0 20px 40px rgba(0,0,0,0.1)",
}}
transition={{
type: "spring",
stiffness: 300,
damping: 20,
}}
>
Card content
</motion.div>Stacked Cards
const cards = [1, 2, 3];
<div className="relative">
{cards.map((card, i) => (
<motion.div
key={card}
className="card absolute"
initial={{ y: i * 10, scale: 1 - i * 0.05 }}
whileHover={{ y: i * -20, scale: 1 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
>
Card {card}
</motion.div>
))}
</div>;List Animations
Stagger Children
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const item = {
hidden: { opacity: 0, x: -20 },
show: { opacity: 1, x: 0 },
};
<motion.ul variants={container} initial="hidden" animate="show">
{items.map((item) => (
<motion.li key={item.id} variants={item}>
{item.text}
</motion.li>
))}
</motion.ul>;List Item Exit
import { AnimatePresence } from "framer-motion";
<AnimatePresence>
{items.map((item) => (
<motion.li
key={item.id}
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
>
{item.text}
</motion.li>
))}
</AnimatePresence>;Modal Animations
Fade + Scale Modal
import { AnimatePresence } from "framer-motion";
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
className="fixed inset-0 bg-black/50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setIsOpen(false)}
/>
{/* Modal */}
<motion.div
className="fixed inset-0 flex items-center justify-center"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
>
<div className="modal-content">Modal content</div>
</motion.div>
</>
)}
</AnimatePresence>;Slide-In Modal
<motion.div
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
>
Modal content
</motion.div>Scroll Animations
Reveal on Scroll
import { motion, useInView } from "framer-motion";
import { useRef } from "react";
const ref = useRef(null);
const isInView = useInView(ref, { once: true });
<motion.div
ref={ref}
initial={{ opacity: 0, y: 50 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }}
transition={{ duration: 0.6 }}
>
Content reveals when scrolled into view
</motion.div>;Parallax Effect
import { motion, useScroll, useTransform } from "framer-motion";
const { scrollY } = useScroll();
const y = useTransform(scrollY, [0, 500], [0, 150]);
<motion.div style={{ y }}>Parallax content</motion.div>;Progress Bar
import { motion, useScroll } from "framer-motion";
const { scrollYProgress } = useScroll();
<motion.div className="progress-bar" style={{ scaleX: scrollYProgress }} />;Navigation Animations
Menu Toggle
const [isOpen, setIsOpen] = useState(false);
<motion.button onClick={() => setIsOpen(!isOpen)} animate={isOpen ? "open" : "closed"}>
<motion.span
variants={{
closed: { rotate: 0, y: 0 },
open: { rotate: 45, y: 6 },
}}
/>
<motion.span
variants={{
closed: { opacity: 1 },
open: { opacity: 0 },
}}
/>
<motion.span
variants={{
closed: { rotate: 0, y: 0 },
open: { rotate: -45, y: -6 },
}}
/>
</motion.button>;Dropdown Menu
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
Menu items
</motion.div>
)}
</AnimatePresence>Loading States
Spinner
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 1,
repeat: Infinity,
ease: "linear",
}}
>
<LoadingIcon />
</motion.div>Pulse
<motion.div
animate={{
scale: [1, 1.1, 1],
opacity: [1, 0.8, 1],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
>
Loading...
</motion.div>Skeleton Shimmer
<motion.div
className="skeleton"
animate={{
backgroundPosition: ["200% 0", "-200% 0"],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear",
}}
style={{
background: "linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)",
backgroundSize: "200% 100%",
}}
/>Advanced Patterns
Magnetic Button
import { motion, useMotionValue, useSpring } from "framer-motion";
const x = useMotionValue(0);
const y = useMotionValue(0);
const springConfig = { damping: 25, stiffness: 300 };
const springX = useSpring(x, springConfig);
const springY = useSpring(y, springConfig);
<motion.button
style={{ x: springX, y: springY }}
onMouseMove={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
x.set((e.clientX - centerX) * 0.3);
y.set((e.clientY - centerY) * 0.3);
}}
onMouseLeave={() => {
x.set(0);
y.set(0);
}}
>
Magnetic button
</motion.button>;Smooth Scroll Progress
import { motion, useScroll, useSpring } from "framer-motion";
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001,
});
<motion.div
className="fixed left-0 right-0 top-0 h-1 origin-left bg-blue-500"
style={{ scaleX }}
/>;Page Transition
// layout.tsx
import { AnimatePresence } from "framer-motion";
<AnimatePresence mode="wait">
<motion.div
key={pathname}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
</AnimatePresence>;Tips for Best Results
- Always use transform properties (
x,y,scale) instead of layout properties - Add
will-change: transformCSS for frequently animated elements - Use spring physics for natural, organic movement
- Respect user preferences with
useReducedMotion() - Keep animations under 0.5s for UI feedback
- Test on mobile devices - animations feel different on touch screens
Performance Checklist
- Using transform properties (not width/height/top/left)
- Added
will-change: transformCSS - Using
useMotionValuefor frequently updated values - Wrapped exit animations in
AnimatePresence - Tested with reduced motion preferences
- Verified 60fps on mobile devices
Common Mistakes
❌ Don't animate layout properties:
<motion.div animate={{ width: 200 }} />✅ Use transform instead:
<motion.div animate={{ scale: 1.5 }} />❌ Don't forget AnimatePresence for exits:
{
show && <motion.div exit={{ opacity: 0 }} />;
}✅ Wrap with AnimatePresence:
<AnimatePresence>{show && <motion.div exit={{ opacity: 0 }} />}</AnimatePresence>Happy animating! 🎉