| name | framer-motion-patterns |
| description | Web UI animation authoring for React/Next.js apps. Framer Motion variants, spring physics, gesture interactions, scroll-triggered reveals, page transitions, layout animations, AnimatePresence exit animations. CSS keyframe patterns. Use when building new animations/transitions into web app components — not for video (use remotion-animation) or performance fixes (use fixing-motion-performance). |
Framer Motion Patterns
Production-grade web UI animation for React/Next.js. Apply when adding motion to components, not just fixing existing jank.
When to Apply
- Entrance/exit animations on components
- Gesture interactions (drag, hover, tap, swipe)
- Page/route transitions
- Scroll-triggered reveals and parallax
- Layout animations (list reorder, expand/collapse)
- Loading sequences, skeleton animations, micro-interactions
Not For
- Video/programmatic animation →
remotion-animation
- Fixing animation perf bugs →
fixing-motion-performance
- p5.js generative art →
algorithmic-art
1. Variants (the right choreography pattern)
Variants keep animation state in data. Always prefer them over inline animate props for anything beyond trivial cases.
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.08,
delayChildren: 0.1,
},
},
}
const itemVariants = {
hidden: { opacity: 0, y: 24 },
visible: {
opacity: 1,
y: 0,
transition: { type: "spring", stiffness: 300, damping: 24 },
},
}
<motion.ul variants={containerVariants} initial="hidden" animate="visible">
{items.map((item) => (
<motion.li key={item.id} variants={itemVariants}>
{item.label}
</motion.li>
))}
</motion.ul>
2. Spring Physics (prefer over duration/easing)
Springs feel physical. Avoid arbitrary durations — springs self-document through stiffness and damping.
| Feel | stiffness | damping |
|---|
| Bouncy | 400 | 10 |
| Snappy | 300 | 24 |
| Smooth | 100 | 20 |
| Slow/heavy | 50 | 15 |
transition={{ type: "spring", stiffness: 300, damping: 30 }}
transition={{ type: "spring", stiffness: 400, damping: 10 }}
3. Gesture Interactions
<motion.button
whileHover={{ scale: 1.02, backgroundColor: "#f0f0f0" }}
whileTap={{ scale: 0.98 }}
transition={{ type: "spring", stiffness: 400, damping: 20 }}
>
Click me
</motion.button>
<motion.div
drag
dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
dragElastic={0.2}
whileDrag={{ scale: 1.05, boxShadow: "0 10px 30px rgba(0,0,0,0.2)" }}
/>
<motion.div
drag="x"
dragConstraints={{ left: 0, right: 0 }}
onDragEnd= ) => {
if (Math.abs(info.offset.x) > 100) dismiss()
}}
/>
4. Scroll Animations
import { useInView } from "framer-motion"
import { useRef } from "react"
function RevealOnScroll({ children }: { children: React.ReactNode }) {
const ref = useRef(null)
const isInView = useInView(ref, { once: true, margin: "-100px" })
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 40 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ type: "spring", stiffness: 100, damping: 20 }}
>
{children}
</motion.div>
)
}
import { useScroll, useTransform } from "framer-motion"
function ParallaxHero() {
const { scrollY } = useScroll()
const y = useTransform(scrollY, [0, 500], [0, 150])
const opacity = useTransform(scrollY, [0, 300], [1, 0])
return <motion.div style={{ y, opacity }} className="hero-background" />
}
function ProgressBar() {
const { scrollYProgress } = useScroll()
return (
<motion.div
className="fixed top-0 left-0 right-0 h-1 bg-blue-500 origin-left"
style={{ scaleX: scrollYProgress }}
/>
)
}
5. AnimatePresence + Exit Animations
Exit animations only work inside AnimatePresence.
import { AnimatePresence, motion } from "framer-motion"
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.15 }}
>
{content}
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{items.map((item) => (
<motion.li
key={item.id}
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, }}
= }}
>
{item.content}
))}
6. Layout Animations
import { LayoutGroup, motion } from "framer-motion"
<LayoutGroup>
{items.map((item) => (
<motion.div key={item.id} layout>
{item.content}
</motion.div>
))}
</LayoutGroup>
<motion.div layout="size" className="card">
<motion.h2 layout="position">{title}</motion.h2>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{expandedContent}
</motion.div>
)}
</AnimatePresence>
</motion.div>
7. Page Transitions (Next.js App Router)
"use client"
import { motion } from "framer-motion"
export default function Template({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
>
{children}
</motion.div>
)
}
8. Shared Layout (cross-route element morphing)
<motion.div layoutId={`card-${id}`} className="card-thumbnail" />
<motion.div layoutId={`card-${id}`} className="card-full" />
9. useAnimation (imperative control)
import { useAnimation } from "framer-motion"
const controls = useAnimation()
async function runSequence() {
await controls.start({ x: 100, transition: { duration: 0.3 } })
await controls.start({ rotate: 360, transition: { duration: 0.5 } })
controls.stop()
}
<motion.div animate={controls} />
Common Micro-interaction Presets
const tooltipAnim = {
initial: { opacity: 0, y: 4, scale: 0.95 },
animate: { opacity: 1, y: 0, scale: 1 },
exit: { opacity: 0, y: 4, scale: 0.95 },
transition: { duration: 0.12 }
}
const notificationAnim = {
initial: { opacity: 0, x: 40 },
animate: { opacity: 1, x: 0 },
exit: { opacity: 0, x: 40, transition: { duration: 0.15 } },
transition: { type: "spring", stiffness: 400, damping: 30 }
}
const backdropAnim = {
initial: { opacity: },
: { : },
: { : },
}
modalAnim = {
: { : , : , : },
: { : , : , : },
: { : , : , : },
: { : , : , : }
}
accordionAnim = {
: { : , : },
: { : , : },
: { : , : },
: { : , : }
}
Performance Rules
- Animate only compositor properties —
transform, opacity. Never width, height, top, left, padding.
layout prop is expensive — only use when list reorder/size animation is genuinely needed.
- Respect
prefers-reduced-motion:
import { useReducedMotion } from "framer-motion"
function SafeAnimation({ children }) {
const shouldReduce = useReducedMotion()
return (
<motion.div
initial={{ opacity: 0, y: shouldReduce ? 0 : 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: shouldReduce ? 0.01 : 0.4 }}
>
{children}
</motion.div>
)
}
AnimatePresence is required for exit animations — without it, exit props are silently ignored.
mode="wait" on AnimatePresence when only one child should be visible at a time (e.g., route transitions):
<AnimatePresence mode="wait">
<motion.div key={route} exit={{ opacity: 0 }} />
</AnimatePresence>
Installation
pnpm add framer-motion
npm install framer-motion
Cross-References
fixing-motion-performance — audit/fix jank in existing animations
remotion-animation — programmatic video animation (Remotion library)
frontend-design — visual aesthetics, typography, composition
responsive-design — responsive layout; pair with useReducedMotion
baseline-ui — Tailwind CSS defaults