Design smooth UI animations and interactions using Framer Motion for React/Next.js. Use when users request animations, transitions, hover effects, scroll animations, gesture interactions, or need help implementing Framer Motion. Follow modern motion UX principles with purposeful, performant animations that enhance usability.
Design purposeful, smooth UI animations using Framer Motion following modern motion UX principles.
Motion UX Principles:
When to Animate:
When NOT to Animate:
Required Dependency:
{
"framer-motion": "^11.0.0"
}
Optional (Scroll Animations):
{
"react-intersection-observer": "^9.5.0"
}
When a user requests animation design, follow this approach:
Categorize the interaction into one of these patterns:
1. State Transitions (hover, focus, active, disabled)
2. Enter/Exit Animations (mount, unmount, visibility)
3. Layout Animations (position, size changes)
4. Scroll Animations (scroll-triggered reveals)
5. Gesture Interactions (drag, tap, swipe)
For each animation, specify:
Properties to Animate:
transform, opacitywidth, height, top, left, marginDuration: Target time in milliseconds
Easing: Curve type (see timing guide below)
Delay: Stagger or sequence timing
Spring vs Tween: Natural feel vs precise control
Structure code with:
Use Case: Call-to-action buttons, interactive elements
Implementation:
import { motion } from 'framer-motion'
export function AnimatedButton({ children, onClick }) {
return (
<motion.button
onClick={onClick}
whileHover={{
scale: 1.05,
boxShadow: '0 10px 20px rgba(0, 0, 0, 0.15)'
}}
whileTap={{ scale: 0.95 }}
transition={{
type: 'spring',
stiffness: 400,
damping: 17
}}
style={{
padding: '12px 24px',
backgroundColor: '#6366F1',
color: 'white',
border: 'none',
borderRadius: '8px',
cursor: 'pointer'
}}
>
{children}
</motion.button>
)
}
Motion Properties:
Use Case: Overlays, popups, tooltips
Implementation:
import { motion, AnimatePresence } from 'framer-motion'
export function Modal({ isOpen, onClose, children }) {
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
onClick={onClose}
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 40
}}
/>
{/* Modal Content */}
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{
duration: 0.3,
ease: [0.4, 0, 0.2, 1] // Custom ease-out
}}
style={{
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: 'white',
padding: '32px',
borderRadius: '12px',
zIndex: 50
}}
>
{children}
</motion.div>
</>
)}
</AnimatePresence>
)
}
Motion Properties:
Use Case: Todo lists, navigation items, feature grids
Implementation:
import { motion } from 'framer-motion'
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1, // 100ms delay between children
delayChildren: 0.2 // Wait 200ms before starting
}
}
}
const itemVariants = {
hidden: { opacity: 0, x: -20 },
visible: {
opacity: 1,
x: 0,
transition: {
duration: 0.4,
ease: 'easeOut'
}
}
}
export function TodoList({ todos }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
style={{ listStyle: 'none', padding: 0 }}
>
{todos.map((todo) => (
<motion.li
key={todo.id}
variants={itemVariants}
style={{ padding: '12px', marginBottom: '8px' }}
>
{todo.title}
</motion.li>
))}
</motion.ul>
)
}
Motion Properties:
Use Case: Section reveals, parallax effects
Implementation:
import { motion, useScroll, useTransform } from 'framer-motion'
import { useRef } from 'react'
export function ScrollReveal({ children }) {
const ref = useRef(null)
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.3 }} // Trigger at 30% visible
transition={{ duration: 0.6, ease: 'easeOut' }}
>
{children}
</motion.div>
)
}
// Advanced: Parallax scroll
export function ParallaxSection() {
const { scrollYProgress } = useScroll()
const y = useTransform(scrollYProgress, [0, 1], ['0%', '50%'])
return (
<motion.div
style={{ y }}
className="background-layer"
>
{/* Background content */}
</motion.div>
)
}
Motion Properties:
Use Case: Animated lists, accordions, grid layouts
Implementation:
import { motion, LayoutGroup } from 'framer-motion'
import { useState } from 'react'
export function AnimatedList({ items }) {
const [sortedItems, setSortedItems] = useState(items)
return (
<LayoutGroup>
<motion.ul layout>
{sortedItems.map((item) => (
<motion.li
key={item.id}
layout
transition={{
type: 'spring',
stiffness: 500,
damping: 30
}}
>
{item.title}
</motion.li>
))}
</motion.ul>
</LayoutGroup>
)
}
// Accordion example
export function Accordion({ title, children }) {
const [isOpen, setIsOpen] = useState(false)
return (
<motion.div layout>
<motion.button
layout
onClick={() => setIsOpen(!isOpen)}
>
{title}
</motion.button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
)}
</AnimatePresence>
</motion.div>
)
}
Motion Properties:
Use Case: Sliders, reorderable lists, custom controls
Implementation:
import { motion } from 'framer-motion'
export function DraggableCard() {
return (
<motion.div
drag
dragConstraints={{
top: -100,
left: -100,
right: 100,
bottom: 100
}}
dragElastic={0.1}
dragTransition={{
bounceStiffness: 600,
bounceDamping: 20
}}
whileDrag={{ scale: 1.05, cursor: 'grabbing' }}
style={{
width: 200,
height: 200,
backgroundColor: '#6366F1',
borderRadius: 12,
cursor: 'grab'
}}
>
Drag me!
</motion.div>
)
}
// Swipe to dismiss
export function SwipeCard({ onDismiss }) {
return (
<motion.div
drag="x"
dragConstraints={{ left: 0, right: 0 }}
onDragEnd={(e, { offset, velocity }) => {
const swipe = Math.abs(offset.x) * velocity.x
if (swipe < -10000 || swipe > 10000) {
onDismiss()
}
}}
>
Swipe to dismiss
</motion.div>
)
}
Motion Properties:
Instant (0-100ms):
Quick (100-200ms):
transition={{ duration: 0.15 }}Standard (200-400ms):
transition={{ duration: 0.3 }}Slow (400-600ms):
transition={{ duration: 0.5 }}Very Slow (600ms+):
transition={{ duration: 0.8 }}Ease Out (Most Common):
transition={{ ease: 'easeOut' }}
// or custom: ease: [0.4, 0, 0.2, 1]
Ease In:
transition={{ ease: 'easeIn' }}
Ease In-Out:
transition={{ ease: 'easeInOut' }}
Spring (Most Natural):
transition={{
type: 'spring',
stiffness: 500, // Higher = faster
damping: 30 // Higher = less bounce
}}
Linear:
transition={{ ease: 'linear' }}
Always include reduced motion support:
import { useReducedMotion } from 'framer-motion'
export function AccessibleAnimation({ children }) {
const shouldReduceMotion = useReducedMotion()
const variants = {
hidden: { opacity: 0, y: shouldReduceMotion ? 0 : 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: shouldReduceMotion ? 0.01 : 0.4
}
}
}
return (
<motion.div
variants={variants}
initial="hidden"
animate="visible"
>
{children}
</motion.div>
)
}
What to Reduce:
layout prop)Animate These (60fps guaranteed):
// ✅ Fast (uses transform)
<motion.div
animate={{
x: 100, // translateX
y: 100, // translateY
scale: 1.5, // scale
rotate: 45, // rotate
opacity: 0.5 // opacity
}}
/>
Avoid Animating (causes reflow/repaint):
// ❌ Slow (triggers layout recalculation)
<motion.div
animate={{
width: 300,
height: 200,
top: 100,
left: 100,
margin: 20
}}
/>
For complex animations:
<motion.div
style={{ willChange: 'transform' }}
animate={{ x: 100, rotate: 45 }}
/>
Warning: Only use for actively animating elements. Remove after animation completes.
import { motion, LazyMotion, domAnimation } from 'framer-motion'
// Reduces bundle size by 30KB
export function OptimizedApp() {
return (
<LazyMotion features={domAnimation}>
<motion.div>Optimized animations</motion.div>
</LazyMotion>
)
}
// app/layout.tsx
import { motion, AnimatePresence } from 'framer-motion'
import { usePathname } from 'next/navigation'
export default function Layout({ children }) {
const pathname = usePathname()
return (
<AnimatePresence mode="wait">
<motion.div
key={pathname}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
</AnimatePresence>
)
}
export function LoadingSpinner() {
return (
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 1,
repeat: Infinity,
ease: 'linear'
}}
style={{
width: 40,
height: 40,
border: '3px solid #E5E7EB',
borderTopColor: '#6366F1',
borderRadius: '50%'
}}
/>
)
}
import { motion } from 'framer-motion'
const checkmarkVariants = {
hidden: { pathLength: 0, opacity: 0 },
visible: {
pathLength: 1,
opacity: 1,
transition: {
pathLength: { duration: 0.5, ease: 'easeOut' },
opacity: { duration: 0.01 }
}
}
}
export function SuccessCheckmark() {
return (
<svg width="60" height="60" viewBox="0 0 60 60">
<motion.path
d="M15,30 L25,40 L45,20"
fill="none"
stroke="#10B981"
strokeWidth="4"
strokeLinecap="round"
variants={checkmarkVariants}
initial="hidden"
animate="visible"
/>
</svg>
)
}
When designing animations, verify:
transform and opacity?prefers-reduced-motion?LazyMotion if many animations?For complex scenarios, see:
When providing motion design guidance, structure as:
## Animation: [Component Name]
### Purpose
[Why this animation exists - user benefit]
### Motion Specs
- **Trigger**: [Hover, click, scroll, mount]
- **Properties**: [x, y, scale, opacity, etc.]
- **Duration**: [200ms, 400ms, etc.]
- **Easing**: [ease-out, spring, etc.]
- **Delay**: [If staggered or sequenced]
### Implementation
[Framer Motion code]
### Reduced Motion Fallback
[Simplified version for accessibility]
### Performance Notes
[GPU acceleration, potential issues]
Remember: Less is more. Purposeful motion enhances usability. Excessive animation annoys users.