Framer Motion - Profesjonalne Animacje w React
Czym jest Framer Motion?
Framer Motion to produkcyjna biblioteka animacji dla React, stworzona przez zespół Framer. Wyróżnia się:
- Deklaratywnym API - animacje opisujesz w JSX
- Gesture support - drag, hover, tap, pan
- Layout animations - płynne przejścia między layoutami
- Exit animations - animacje przy odmontowywaniu komponentów
- Server-safe - działa z SSR (Next.js)
- Variants - orkiestracja złożonych animacji
Framer Motion to standard dla animacji w ekosystemie React, używany przez Vercel, Stripe, Netflix i tysiące innych firm.
Dlaczego Framer Motion?
CSS Animations vs Framer Motion
// ❌ CSS - ograniczone możliwości
// Brak: physics-based springs, exit animations,
// layout animations, gesture support
// ✅ Framer Motion - pełna kontrola
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
drag="x"
dragConstraints={{ left: -100, right: 100 }}
layout
/>Instalacja
npm install framer-motionPodstawy Animacji
motion Component
import { motion } from 'framer-motion'
function AnimatedBox() {
return (
<motion.div
// Stan początkowy
initial={{ opacity: 0, scale: 0.8 }}
// Stan docelowy
animate={{ opacity: 1, scale: 1 }}
// Konfiguracja przejścia
transition={{ duration: 0.5, ease: 'easeOut' }}
className="w-32 h-32 bg-blue-500 rounded-lg"
/>
)
}Animowane elementy HTML
// Wszystkie elementy HTML mają swoje motion odpowiedniki
<motion.div />
<motion.span />
<motion.button />
<motion.ul />
<motion.li />
<motion.svg />
<motion.path />
<motion.img />
<motion.a />
// ... i wszystkie inneAnimowane wartości
// Wspierane właściwości
<motion.div
animate={{
// Pozycja
x: 100,
y: 50,
// Skala
scale: 1.2,
scaleX: 1.5,
scaleY: 0.8,
// Obrót
rotate: 180,
rotateX: 45,
rotateY: 45,
rotateZ: 90,
// Przezroczystość
opacity: 0.5,
// Kolory
backgroundColor: '#ff0000',
color: '#ffffff',
borderColor: '#000000',
// Border radius
borderRadius: '50%',
// Cienie
boxShadow: '0 10px 20px rgba(0,0,0,0.3)',
// Wymiary
width: '200px',
height: '100px',
// Padding, margin
padding: 20,
margin: 10,
}}
/>Transitions - Rodzaje Przejść
Spring (domyślne)
<motion.div
animate={{ x: 100 }}
transition={{
type: 'spring',
stiffness: 100, // Sztywność sprężyny
damping: 10, // Tłumienie
mass: 1, // Masa
velocity: 2, // Początkowa prędkość
restDelta: 0.01, // Próg zatrzymania
restSpeed: 0.01,
}}
/>Tween (klasyczne easing)
<motion.div
animate={{ x: 100 }}
transition={{
type: 'tween',
duration: 0.5,
ease: 'easeInOut',
// Lub custom cubic-bezier
ease: [0.6, 0.01, 0.05, 0.95],
}}
/>
// Dostępne easing:
// 'linear', 'easeIn', 'easeOut', 'easeInOut'
// 'circIn', 'circOut', 'circInOut'
// 'backIn', 'backOut', 'backInOut'
// 'anticipate'Inertia (dla drag)
<motion.div
drag
dragTransition={{
type: 'inertia',
power: 0.8, // Siła inercji
timeConstant: 700, // Czas trwania
modifyTarget: (target) => Math.round(target / 100) * 100, // Snap do grid
}}
/>Delay i Stagger
<motion.div
animate={{ x: 100 }}
transition={{
delay: 0.5, // Opóźnienie startu
delayChildren: 0.3, // Opóźnienie dla dzieci
staggerChildren: 0.1, // Odstęp między dziećmi
staggerDirection: 1, // 1 lub -1
}}
/>Interaktywność - Gestures
Hover i Tap
<motion.button
whileHover={{
scale: 1.05,
backgroundColor: '#3b82f6',
}}
whileTap={{
scale: 0.95,
}}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
className="px-6 py-3 bg-blue-500 text-white rounded-lg"
>
Kliknij mnie
</motion.button>Focus
<motion.input
whileFocus={{
scale: 1.02,
boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.5)',
}}
className="px-4 py-2 border rounded"
placeholder="Wpisz tekst..."
/>Drag
function DraggableCard() {
return (
<motion.div
drag // Włącz drag (true, "x", "y")
dragConstraints={{ // Ograniczenia
top: -100,
left: -100,
right: 100,
bottom: 100,
}}
dragElastic={0.2} // Elastyczność na granicy (0-1)
dragMomentum={true} // Inercja po puszczeniu
dragSnapToOrigin // Wróć do pozycji początkowej
onDragStart={(event, info) => console.log('Start:', info.point)}
onDrag={(event, info) => console.log('Drag:', info.offset)}
onDragEnd={(event, info) => console.log('End:', info.velocity)}
className="w-32 h-32 bg-purple-500 rounded-lg cursor-grab active:cursor-grabbing"
/>
)
}Drag z constraints ref
function DragInContainer() {
const constraintsRef = useRef(null)
return (
<motion.div
ref={constraintsRef}
className="w-96 h-96 bg-gray-100 rounded-xl overflow-hidden"
>
<motion.div
drag
dragConstraints={constraintsRef}
className="w-20 h-20 bg-blue-500 rounded-lg"
/>
</motion.div>
)
}Pan Gesture
<motion.div
onPan={(event, info) => {
console.log('Delta:', info.delta)
console.log('Offset:', info.offset)
console.log('Velocity:', info.velocity)
}}
onPanStart={(event, info) => console.log('Pan started')}
onPanEnd={(event, info) => console.log('Pan ended')}
/>Variants - Orkiestracja Animacji
Podstawowe variants
const boxVariants = {
hidden: {
opacity: 0,
y: 20,
},
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
},
},
hover: {
scale: 1.05,
boxShadow: '0 10px 30px rgba(0,0,0,0.2)',
},
}
function AnimatedCard() {
return (
<motion.div
variants={boxVariants}
initial="hidden"
animate="visible"
whileHover="hover"
className="p-6 bg-white rounded-lg shadow"
>
Card content
</motion.div>
)
}Propagacja do dzieci
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1, // Odstęp między dziećmi
delayChildren: 0.2, // Opóźnienie przed pierwszym dzieckiem
},
},
}
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.3 },
},
}
function StaggeredList({ items }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-4"
>
{items.map((item) => (
<motion.li
key={item.id}
variants={itemVariants}
className="p-4 bg-white rounded shadow"
>
{item.name}
</motion.li>
))}
</motion.ul>
)
}Dynamic Variants
const itemVariants = {
hidden: (custom: number) => ({
opacity: 0,
x: custom * 50,
}),
visible: (custom: number) => ({
opacity: 1,
x: 0,
transition: {
delay: custom * 0.1,
},
}),
}
function DynamicList({ items }) {
return (
<motion.ul initial="hidden" animate="visible">
{items.map((item, index) => (
<motion.li
key={item.id}
custom={index} // Przekaż custom value
variants={itemVariants}
>
{item.name}
</motion.li>
))}
</motion.ul>
)
}AnimatePresence - Exit Animations
Podstawowe użycie
import { motion, AnimatePresence } from 'framer-motion'
function Modal({ isOpen, onClose }) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 flex items-center justify-center"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
transition={{ type: 'spring', damping: 20 }}
className="bg-white p-8 rounded-xl max-w-md w-full"
onClick={(e) => e.stopPropagation()}
>
<h2>Modal Title</h2>
<p>Modal content goes here...</p>
<button onClick={onClose}>Close</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
)
}Mode dla listy
function NotificationList({ notifications }) {
return (
<AnimatePresence mode="popLayout">
{notifications.map((notification) => (
<motion.div
key={notification.id}
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="mb-2"
>
<Notification {...notification} />
</motion.div>
))}
</AnimatePresence>
)
}
// Tryby AnimatePresence:
// "sync" (domyślny) - exit i enter jednocześnie
// "wait" - czekaj aż exit się skończy
// "popLayout" - automatycznie zarządza layoutemPage Transitions (Next.js)
// app/template.tsx
'use client'
import { motion, AnimatePresence } from 'framer-motion'
import { usePathname } from 'next/navigation'
export default function Template({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
return (
<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>
)
}Layout Animations
Podstawowe layout
function ExpandableCard() {
const [isExpanded, setIsExpanded] = useState(false)
return (
<motion.div
layout
onClick={() => setIsExpanded(!isExpanded)}
className={`bg-blue-500 rounded-lg p-4 cursor-pointer ${
isExpanded ? 'w-64 h-64' : 'w-32 h-32'
}`}
>
<motion.h2 layout="position">Title</motion.h2>
{isExpanded && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
Expanded content...
</motion.p>
)}
</motion.div>
)
}Shared Layout Animation
function Tabs() {
const [selected, setSelected] = useState(0)
const tabs = ['Home', 'About', 'Contact']
return (
<div className="flex gap-2">
{tabs.map((tab, index) => (
<button
key={tab}
onClick={() => setSelected(index)}
className="relative px-4 py-2"
>
{tab}
{selected === index && (
<motion.div
layoutId="underline"
className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-500"
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
/>
)}
</button>
))}
</div>
)
}LayoutGroup
import { LayoutGroup } from 'framer-motion'
function CardGrid() {
const [cards, setCards] = useState(initialCards)
return (
<LayoutGroup>
<div className="grid grid-cols-3 gap-4">
{cards.map((card) => (
<motion.div
key={card.id}
layout
layoutId={`card-${card.id}`}
className="p-4 bg-white rounded shadow"
>
{card.title}
</motion.div>
))}
</div>
</LayoutGroup>
)
}Keyframes
Sekwencja wartości
<motion.div
animate={{
scale: [1, 1.2, 1.2, 1, 1],
rotate: [0, 0, 180, 180, 0],
borderRadius: ['0%', '0%', '50%', '50%', '0%'],
}}
transition={{
duration: 2,
ease: 'easeInOut',
times: [0, 0.2, 0.5, 0.8, 1], // Kontrola timing
repeat: Infinity,
repeatDelay: 1,
}}
/>Keyframes z obiektami
<motion.div
animate={{
x: [0, 100, 0],
backgroundColor: ['#ff0000', '#00ff00', '#0000ff'],
}}
transition={{
duration: 3,
repeat: Infinity,
}}
/>useAnimate Hook
Programowe animacje
import { useAnimate } from 'framer-motion'
function AnimatedComponent() {
const [scope, animate] = useAnimate()
async function handleClick() {
// Sekwencja animacji
await animate(scope.current, { scale: 1.2 }, { duration: 0.2 })
await animate(scope.current, { rotate: 180 }, { duration: 0.3 })
await animate(scope.current, { scale: 1, rotate: 0 }, { duration: 0.2 })
}
return (
<div ref={scope}>
<motion.div
className="w-32 h-32 bg-blue-500 rounded-lg"
onClick={handleClick}
/>
</div>
)
}Animowanie wielu elementów
function StaggeredAnimation() {
const [scope, animate] = useAnimate()
async function handleAnimate() {
await animate(
'li', // Selektor
{ opacity: 1, x: 0 }, // Animacja
{ delay: stagger(0.1) } // Opcje
)
}
return (
<ul ref={scope}>
<li style={{ opacity: 0, x: -20 }}>Item 1</li>
<li style={{ opacity: 0, x: -20 }}>Item 2</li>
<li style={{ opacity: 0, x: -20 }}>Item 3</li>
</ul>
)
}useMotionValue i useTransform
Śledzenie wartości
import { motion, useMotionValue, useTransform } from 'framer-motion'
function ParallaxCard() {
const x = useMotionValue(0)
const y = useMotionValue(0)
// Transformuj wartości
const rotateX = useTransform(y, [-100, 100], [30, -30])
const rotateY = useTransform(x, [-100, 100], [-30, 30])
return (
<motion.div
style={{ x, y, rotateX, rotateY }}
drag
dragElastic={0.1}
dragConstraints={{ top: 0, left: 0, right: 0, bottom: 0 }}
className="w-64 h-80 bg-gradient-to-br from-purple-500 to-pink-500 rounded-xl"
/>
)
}useMotionValueEvent
import { useMotionValue, useMotionValueEvent } from 'framer-motion'
function ScrollProgress() {
const scrollY = useMotionValue(0)
useMotionValueEvent(scrollY, 'change', (latest) => {
console.log('Scroll position:', latest)
})
return (/* ... */)
}useScroll Hook
Scroll progress
import { motion, useScroll, useTransform } from 'framer-motion'
function ScrollProgressBar() {
const { scrollYProgress } = useScroll()
return (
<motion.div
className="fixed top-0 left-0 right-0 h-1 bg-blue-500 origin-left"
style={{ scaleX: scrollYProgress }}
/>
)
}Scroll-linked animations
function ParallaxSection() {
const ref = useRef(null)
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start end', 'end start'],
})
const y = useTransform(scrollYProgress, [0, 1], [100, -100])
const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0])
return (
<div ref={ref} className="h-screen relative overflow-hidden">
<motion.div
style={{ y, opacity }}
className="absolute inset-0 flex items-center justify-center"
>
<h1 className="text-6xl font-bold">Parallax Text</h1>
</motion.div>
</div>
)
}Scroll velocity
import { useScroll, useVelocity, useTransform } from 'framer-motion'
function VelocityText() {
const { scrollY } = useScroll()
const scrollVelocity = useVelocity(scrollY)
const skewX = useTransform(scrollVelocity, [-1000, 0, 1000], [-10, 0, 10])
return (
<motion.h1
style={{ skewX }}
className="text-4xl font-bold"
>
Velocity-based skew
</motion.h1>
)
}useInView Hook
Animacja przy wejściu w viewport
import { motion, useInView } from 'framer-motion'
import { useRef } from 'react'
function FadeInSection({ children }) {
const ref = useRef(null)
const isInView = useInView(ref, {
once: true, // Animuj tylko raz
margin: '-100px', // Offset
amount: 0.5, // Procent widoczności (0-1)
})
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 50 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }}
transition={{ duration: 0.5 }}
>
{children}
</motion.div>
)
}whileInView
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true, amount: 0.8 }}
transition={{ duration: 0.5 }}
>
Content that animates when visible
</motion.div>SVG Animations
Path animation
function DrawPath() {
return (
<motion.svg width="200" height="200" viewBox="0 0 200 200">
<motion.path
d="M 10 80 Q 100 10 190 80 T 190 80"
fill="transparent"
stroke="#3b82f6"
strokeWidth="4"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 2, ease: 'easeInOut' }}
/>
</motion.svg>
)
}Animated icon
const checkmarkVariants = {
hidden: {
pathLength: 0,
opacity: 0,
},
visible: {
pathLength: 1,
opacity: 1,
transition: {
duration: 0.5,
ease: 'easeOut',
},
},
}
function AnimatedCheckmark() {
return (
<motion.svg
width="50"
height="50"
viewBox="0 0 50 50"
initial="hidden"
animate="visible"
>
<motion.circle
cx="25"
cy="25"
r="20"
fill="none"
stroke="#10b981"
strokeWidth="3"
variants={checkmarkVariants}
/>
<motion.path
d="M 14 25 L 22 33 L 36 17"
fill="none"
stroke="#10b981"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
variants={checkmarkVariants}
/>
</motion.svg>
)
}Praktyczne Przykłady
Animated Modal
const backdropVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1 },
}
const modalVariants = {
hidden: {
opacity: 0,
scale: 0.8,
y: 50,
},
visible: {
opacity: 1,
scale: 1,
y: 0,
transition: {
type: 'spring',
damping: 25,
stiffness: 300,
},
},
exit: {
opacity: 0,
scale: 0.8,
y: 50,
transition: {
duration: 0.2,
},
},
}
function Modal({ isOpen, onClose, children }) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
variants={backdropVariants}
initial="hidden"
animate="visible"
exit="hidden"
className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50"
onClick={onClose}
>
<motion.div
variants={modalVariants}
initial="hidden"
animate="visible"
exit="exit"
className="bg-white rounded-2xl p-8 max-w-lg w-full mx-4 shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
{children}
</motion.div>
</motion.div>
)}
</AnimatePresence>
)
}Animated Accordion
function Accordion({ items }) {
const [openIndex, setOpenIndex] = useState<number | null>(null)
return (
<div className="space-y-2">
{items.map((item, index) => (
<div key={index} className="border rounded-lg overflow-hidden">
<button
onClick={() => setOpenIndex(openIndex === index ? null : index)}
className="w-full p-4 text-left flex justify-between items-center"
>
<span className="font-medium">{item.title}</span>
<motion.span
animate={{ rotate: openIndex === index ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
▼
</motion.span>
</button>
<AnimatePresence>
{openIndex === index && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3 }}
>
<div className="p-4 pt-0 text-gray-600">
{item.content}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
))}
</div>
)
}Animated Counter
function AnimatedCounter({ value }: { value: number }) {
const count = useMotionValue(0)
const rounded = useTransform(count, (latest) => Math.round(latest))
const displayValue = useTransform(rounded, (latest) =>
latest.toLocaleString()
)
useEffect(() => {
const animation = animate(count, value, {
duration: 2,
ease: 'easeOut',
})
return animation.stop
}, [value, count])
return (
<motion.span className="text-4xl font-bold tabular-nums">
{displayValue}
</motion.span>
)
}Animated Toast
const toastVariants = {
initial: {
opacity: 0,
y: 50,
scale: 0.3,
},
animate: {
opacity: 1,
y: 0,
scale: 1,
transition: {
type: 'spring',
stiffness: 400,
damping: 25,
},
},
exit: {
opacity: 0,
x: 100,
transition: {
duration: 0.2,
},
},
}
function ToastContainer({ toasts, removeToast }) {
return (
<div className="fixed bottom-4 right-4 space-y-2 z-50">
<AnimatePresence mode="popLayout">
{toasts.map((toast) => (
<motion.div
key={toast.id}
layout
variants={toastVariants}
initial="initial"
animate="animate"
exit="exit"
className="bg-white shadow-lg rounded-lg p-4 min-w-[300px]"
>
<p>{toast.message}</p>
<button
onClick={() => removeToast(toast.id)}
className="absolute top-2 right-2"
>
×
</button>
</motion.div>
))}
</AnimatePresence>
</div>
)
}Optymalizacja Wydajności
willChange
// Framer Motion automatycznie dodaje will-change
// Ale możesz kontrolować to ręcznie:
<motion.div
style={{ willChange: 'transform' }}
animate={{ x: 100 }}
/>Reduce Motion
import { useReducedMotion } from 'framer-motion'
function AccessibleAnimation() {
const shouldReduceMotion = useReducedMotion()
return (
<motion.div
animate={{
x: shouldReduceMotion ? 0 : 100,
opacity: 1,
}}
/>
)
}LazyMotion
import { LazyMotion, domAnimation, m } from 'framer-motion'
// Mniejszy bundle - ładuje tylko używane features
function App() {
return (
<LazyMotion features={domAnimation}>
<m.div animate={{ opacity: 1 }} />
</LazyMotion>
)
}Framer Motion vs Alternatywy
| Cecha | Framer Motion | React Spring | GSAP |
|---|---|---|---|
| Bundle size | ~40KB | ~25KB | ~60KB |
| API | Deklaratywne | Hook-based | Imperatywne |
| Exit animations | ✅ Native | ⚠️ Trudne | ⚠️ Manualne |
| Layout animations | ✅ Native | ❌ | ⚠️ Plugin |
| Gestures | ✅ Wbudowane | ❌ | ⚠️ Plugin |
| SSR | ✅ | ✅ | ⚠️ |
| Learning curve | Łatwy | Średni | Trudny |
FAQ
Czy Framer Motion działa z Next.js?
Tak! Wspiera SSR i App Router. Użyj 'use client' dla komponentów z animacjami.
Jak animować przy routingu?
Użyj AnimatePresence z template.tsx w Next.js App Router.
Czy mogę używać z TypeScript?
Tak, Framer Motion ma pełne typy TypeScript.
Jak debugować animacje?
Użyj transition={{ duration: 2 }} do spowolnienia animacji. Sprawdź DevTools Performance.
Podsumowanie
Framer Motion to najlepsza biblioteka animacji dla React:
- Proste API - animacje w JSX
- Kompletność - gestures, layout, exit animations
- Wydajność - optymalizacje out of the box
- TypeScript - pełne wsparcie
- Aktywny rozwój - regularne aktualizacje
Jeśli budujesz aplikację React i potrzebujesz animacji - Framer Motion to standard.
Framer Motion - Professional Animations in React
What is Framer Motion?
Framer Motion is a production-ready animation library for React, created by the Framer team. It stands out with:
- Declarative API - you describe animations in JSX
- Gesture support - drag, hover, tap, pan
- Layout animations - smooth transitions between layouts
- Exit animations - animations when unmounting components
- Server-safe - works with SSR (Next.js)
- Variants - orchestration of complex animations
Framer Motion is the standard for animations in the React ecosystem, used by Vercel, Stripe, Netflix, and thousands of other companies.
Why Framer Motion?
CSS Animations vs Framer Motion
// ❌ CSS - limited capabilities
// Missing: physics-based springs, exit animations,
// layout animations, gesture support
// ✅ Framer Motion - full control
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
drag="x"
dragConstraints={{ left: -100, right: 100 }}
layout
/>Installation
npm install framer-motionAnimation Basics
motion Component
import { motion } from 'framer-motion'
function AnimatedBox() {
return (
<motion.div
// Initial state
initial={{ opacity: 0, scale: 0.8 }}
// Target state
animate={{ opacity: 1, scale: 1 }}
// Transition configuration
transition={{ duration: 0.5, ease: 'easeOut' }}
className="w-32 h-32 bg-blue-500 rounded-lg"
/>
)
}Animated HTML elements
// All HTML elements have their motion equivalents
<motion.div />
<motion.span />
<motion.button />
<motion.ul />
<motion.li />
<motion.svg />
<motion.path />
<motion.img />
<motion.a />
// ... and all othersAnimated values
// Supported properties
<motion.div
animate={{
// Position
x: 100,
y: 50,
// Scale
scale: 1.2,
scaleX: 1.5,
scaleY: 0.8,
// Rotation
rotate: 180,
rotateX: 45,
rotateY: 45,
rotateZ: 90,
// Opacity
opacity: 0.5,
// Colors
backgroundColor: '#ff0000',
color: '#ffffff',
borderColor: '#000000',
// Border radius
borderRadius: '50%',
// Shadows
boxShadow: '0 10px 20px rgba(0,0,0,0.3)',
// Dimensions
width: '200px',
height: '100px',
// Padding, margin
padding: 20,
margin: 10,
}}
/>Transitions - Types of Transitions
Spring (default)
<motion.div
animate={{ x: 100 }}
transition={{
type: 'spring',
stiffness: 100, // Spring stiffness
damping: 10, // Damping
mass: 1, // Mass
velocity: 2, // Initial velocity
restDelta: 0.01, // Stopping threshold
restSpeed: 0.01,
}}
/>Tween (classic easing)
<motion.div
animate={{ x: 100 }}
transition={{
type: 'tween',
duration: 0.5,
ease: 'easeInOut',
// Or custom cubic-bezier
ease: [0.6, 0.01, 0.05, 0.95],
}}
/>
// Available easings:
// 'linear', 'easeIn', 'easeOut', 'easeInOut'
// 'circIn', 'circOut', 'circInOut'
// 'backIn', 'backOut', 'backInOut'
// 'anticipate'Inertia (for drag)
<motion.div
drag
dragTransition={{
type: 'inertia',
power: 0.8, // Inertia strength
timeConstant: 700, // Duration
modifyTarget: (target) => Math.round(target / 100) * 100, // Snap to grid
}}
/>Delay and Stagger
<motion.div
animate={{ x: 100 }}
transition={{
delay: 0.5, // Start delay
delayChildren: 0.3, // Delay for children
staggerChildren: 0.1, // Interval between children
staggerDirection: 1, // 1 or -1
}}
/>Interactivity - Gestures
Hover and Tap
<motion.button
whileHover={{
scale: 1.05,
backgroundColor: '#3b82f6',
}}
whileTap={{
scale: 0.95,
}}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
className="px-6 py-3 bg-blue-500 text-white rounded-lg"
>
Click me
</motion.button>Focus
<motion.input
whileFocus={{
scale: 1.02,
boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.5)',
}}
className="px-4 py-2 border rounded"
placeholder="Enter text..."
/>Drag
function DraggableCard() {
return (
<motion.div
drag // Enable drag (true, "x", "y")
dragConstraints={{ // Constraints
top: -100,
left: -100,
right: 100,
bottom: 100,
}}
dragElastic={0.2} // Elasticity at boundary (0-1)
dragMomentum={true} // Inertia after release
dragSnapToOrigin // Return to initial position
onDragStart={(event, info) => console.log('Start:', info.point)}
onDrag={(event, info) => console.log('Drag:', info.offset)}
onDragEnd={(event, info) => console.log('End:', info.velocity)}
className="w-32 h-32 bg-purple-500 rounded-lg cursor-grab active:cursor-grabbing"
/>
)
}Drag with constraints ref
function DragInContainer() {
const constraintsRef = useRef(null)
return (
<motion.div
ref={constraintsRef}
className="w-96 h-96 bg-gray-100 rounded-xl overflow-hidden"
>
<motion.div
drag
dragConstraints={constraintsRef}
className="w-20 h-20 bg-blue-500 rounded-lg"
/>
</motion.div>
)
}Pan Gesture
<motion.div
onPan={(event, info) => {
console.log('Delta:', info.delta)
console.log('Offset:', info.offset)
console.log('Velocity:', info.velocity)
}}
onPanStart={(event, info) => console.log('Pan started')}
onPanEnd={(event, info) => console.log('Pan ended')}
/>Variants - Animation Orchestration
Basic variants
const boxVariants = {
hidden: {
opacity: 0,
y: 20,
},
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
},
},
hover: {
scale: 1.05,
boxShadow: '0 10px 30px rgba(0,0,0,0.2)',
},
}
function AnimatedCard() {
return (
<motion.div
variants={boxVariants}
initial="hidden"
animate="visible"
whileHover="hover"
className="p-6 bg-white rounded-lg shadow"
>
Card content
</motion.div>
)
}Propagation to children
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1, // Interval between children
delayChildren: 0.2, // Delay before the first child
},
},
}
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.3 },
},
}
function StaggeredList({ items }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-4"
>
{items.map((item) => (
<motion.li
key={item.id}
variants={itemVariants}
className="p-4 bg-white rounded shadow"
>
{item.name}
</motion.li>
))}
</motion.ul>
)
}Dynamic Variants
const itemVariants = {
hidden: (custom: number) => ({
opacity: 0,
x: custom * 50,
}),
visible: (custom: number) => ({
opacity: 1,
x: 0,
transition: {
delay: custom * 0.1,
},
}),
}
function DynamicList({ items }) {
return (
<motion.ul initial="hidden" animate="visible">
{items.map((item, index) => (
<motion.li
key={item.id}
custom={index} // Pass custom value
variants={itemVariants}
>
{item.name}
</motion.li>
))}
</motion.ul>
)
}AnimatePresence - Exit Animations
Basic usage
import { motion, AnimatePresence } from 'framer-motion'
function Modal({ isOpen, onClose }) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 flex items-center justify-center"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
transition={{ type: 'spring', damping: 20 }}
className="bg-white p-8 rounded-xl max-w-md w-full"
onClick={(e) => e.stopPropagation()}
>
<h2>Modal Title</h2>
<p>Modal content goes here...</p>
<button onClick={onClose}>Close</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
)
}Mode for lists
function NotificationList({ notifications }) {
return (
<AnimatePresence mode="popLayout">
{notifications.map((notification) => (
<motion.div
key={notification.id}
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="mb-2"
>
<Notification {...notification} />
</motion.div>
))}
</AnimatePresence>
)
}
// AnimatePresence modes:
// "sync" (default) - exit and enter simultaneously
// "wait" - wait until exit finishes
// "popLayout" - automatically manages layoutPage Transitions (Next.js)
// app/template.tsx
'use client'
import { motion, AnimatePresence } from 'framer-motion'
import { usePathname } from 'next/navigation'
export default function Template({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
return (
<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>
)
}Layout Animations
Basic layout
function ExpandableCard() {
const [isExpanded, setIsExpanded] = useState(false)
return (
<motion.div
layout
onClick={() => setIsExpanded(!isExpanded)}
className={`bg-blue-500 rounded-lg p-4 cursor-pointer ${
isExpanded ? 'w-64 h-64' : 'w-32 h-32'
}`}
>
<motion.h2 layout="position">Title</motion.h2>
{isExpanded && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
Expanded content...
</motion.p>
)}
</motion.div>
)
}Shared Layout Animation
function Tabs() {
const [selected, setSelected] = useState(0)
const tabs = ['Home', 'About', 'Contact']
return (
<div className="flex gap-2">
{tabs.map((tab, index) => (
<button
key={tab}
onClick={() => setSelected(index)}
className="relative px-4 py-2"
>
{tab}
{selected === index && (
<motion.div
layoutId="underline"
className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-500"
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
/>
)}
</button>
))}
</div>
)
}LayoutGroup
import { LayoutGroup } from 'framer-motion'
function CardGrid() {
const [cards, setCards] = useState(initialCards)
return (
<LayoutGroup>
<div className="grid grid-cols-3 gap-4">
{cards.map((card) => (
<motion.div
key={card.id}
layout
layoutId={`card-${card.id}`}
className="p-4 bg-white rounded shadow"
>
{card.title}
</motion.div>
))}
</div>
</LayoutGroup>
)
}Keyframes
Value sequences
<motion.div
animate={{
scale: [1, 1.2, 1.2, 1, 1],
rotate: [0, 0, 180, 180, 0],
borderRadius: ['0%', '0%', '50%', '50%', '0%'],
}}
transition={{
duration: 2,
ease: 'easeInOut',
times: [0, 0.2, 0.5, 0.8, 1], // Timing control
repeat: Infinity,
repeatDelay: 1,
}}
/>Keyframes with objects
<motion.div
animate={{
x: [0, 100, 0],
backgroundColor: ['#ff0000', '#00ff00', '#0000ff'],
}}
transition={{
duration: 3,
repeat: Infinity,
}}
/>useAnimate Hook
Programmatic animations
import { useAnimate } from 'framer-motion'
function AnimatedComponent() {
const [scope, animate] = useAnimate()
async function handleClick() {
// Animation sequence
await animate(scope.current, { scale: 1.2 }, { duration: 0.2 })
await animate(scope.current, { rotate: 180 }, { duration: 0.3 })
await animate(scope.current, { scale: 1, rotate: 0 }, { duration: 0.2 })
}
return (
<div ref={scope}>
<motion.div
className="w-32 h-32 bg-blue-500 rounded-lg"
onClick={handleClick}
/>
</div>
)
}Animating multiple elements
function StaggeredAnimation() {
const [scope, animate] = useAnimate()
async function handleAnimate() {
await animate(
'li', // Selector
{ opacity: 1, x: 0 }, // Animation
{ delay: stagger(0.1) } // Options
)
}
return (
<ul ref={scope}>
<li style={{ opacity: 0, x: -20 }}>Item 1</li>
<li style={{ opacity: 0, x: -20 }}>Item 2</li>
<li style={{ opacity: 0, x: -20 }}>Item 3</li>
</ul>
)
}useMotionValue and useTransform
Tracking values
import { motion, useMotionValue, useTransform } from 'framer-motion'
function ParallaxCard() {
const x = useMotionValue(0)
const y = useMotionValue(0)
// Transform values
const rotateX = useTransform(y, [-100, 100], [30, -30])
const rotateY = useTransform(x, [-100, 100], [-30, 30])
return (
<motion.div
style={{ x, y, rotateX, rotateY }}
drag
dragElastic={0.1}
dragConstraints={{ top: 0, left: 0, right: 0, bottom: 0 }}
className="w-64 h-80 bg-gradient-to-br from-purple-500 to-pink-500 rounded-xl"
/>
)
}useMotionValueEvent
import { useMotionValue, useMotionValueEvent } from 'framer-motion'
function ScrollProgress() {
const scrollY = useMotionValue(0)
useMotionValueEvent(scrollY, 'change', (latest) => {
console.log('Scroll position:', latest)
})
return (/* ... */)
}useScroll Hook
Scroll progress
import { motion, useScroll, useTransform } from 'framer-motion'
function ScrollProgressBar() {
const { scrollYProgress } = useScroll()
return (
<motion.div
className="fixed top-0 left-0 right-0 h-1 bg-blue-500 origin-left"
style={{ scaleX: scrollYProgress }}
/>
)
}Scroll-linked animations
function ParallaxSection() {
const ref = useRef(null)
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start end', 'end start'],
})
const y = useTransform(scrollYProgress, [0, 1], [100, -100])
const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0])
return (
<div ref={ref} className="h-screen relative overflow-hidden">
<motion.div
style={{ y, opacity }}
className="absolute inset-0 flex items-center justify-center"
>
<h1 className="text-6xl font-bold">Parallax Text</h1>
</motion.div>
</div>
)
}Scroll velocity
import { useScroll, useVelocity, useTransform } from 'framer-motion'
function VelocityText() {
const { scrollY } = useScroll()
const scrollVelocity = useVelocity(scrollY)
const skewX = useTransform(scrollVelocity, [-1000, 0, 1000], [-10, 0, 10])
return (
<motion.h1
style={{ skewX }}
className="text-4xl font-bold"
>
Velocity-based skew
</motion.h1>
)
}useInView Hook
Animation on entering the viewport
import { motion, useInView } from 'framer-motion'
import { useRef } from 'react'
function FadeInSection({ children }) {
const ref = useRef(null)
const isInView = useInView(ref, {
once: true, // Animate only once
margin: '-100px', // Offset
amount: 0.5, // Visibility percentage (0-1)
})
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 50 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }}
transition={{ duration: 0.5 }}
>
{children}
</motion.div>
)
}whileInView
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true, amount: 0.8 }}
transition={{ duration: 0.5 }}
>
Content that animates when visible
</motion.div>SVG Animations
Path animation
function DrawPath() {
return (
<motion.svg width="200" height="200" viewBox="0 0 200 200">
<motion.path
d="M 10 80 Q 100 10 190 80 T 190 80"
fill="transparent"
stroke="#3b82f6"
strokeWidth="4"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 2, ease: 'easeInOut' }}
/>
</motion.svg>
)
}Animated icon
const checkmarkVariants = {
hidden: {
pathLength: 0,
opacity: 0,
},
visible: {
pathLength: 1,
opacity: 1,
transition: {
duration: 0.5,
ease: 'easeOut',
},
},
}
function AnimatedCheckmark() {
return (
<motion.svg
width="50"
height="50"
viewBox="0 0 50 50"
initial="hidden"
animate="visible"
>
<motion.circle
cx="25"
cy="25"
r="20"
fill="none"
stroke="#10b981"
strokeWidth="3"
variants={checkmarkVariants}
/>
<motion.path
d="M 14 25 L 22 33 L 36 17"
fill="none"
stroke="#10b981"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
variants={checkmarkVariants}
/>
</motion.svg>
)
}Practical Examples
Animated Modal
const backdropVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1 },
}
const modalVariants = {
hidden: {
opacity: 0,
scale: 0.8,
y: 50,
},
visible: {
opacity: 1,
scale: 1,
y: 0,
transition: {
type: 'spring',
damping: 25,
stiffness: 300,
},
},
exit: {
opacity: 0,
scale: 0.8,
y: 50,
transition: {
duration: 0.2,
},
},
}
function Modal({ isOpen, onClose, children }) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
variants={backdropVariants}
initial="hidden"
animate="visible"
exit="hidden"
className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50"
onClick={onClose}
>
<motion.div
variants={modalVariants}
initial="hidden"
animate="visible"
exit="exit"
className="bg-white rounded-2xl p-8 max-w-lg w-full mx-4 shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
{children}
</motion.div>
</motion.div>
)}
</AnimatePresence>
)
}Animated Accordion
function Accordion({ items }) {
const [openIndex, setOpenIndex] = useState<number | null>(null)
return (
<div className="space-y-2">
{items.map((item, index) => (
<div key={index} className="border rounded-lg overflow-hidden">
<button
onClick={() => setOpenIndex(openIndex === index ? null : index)}
className="w-full p-4 text-left flex justify-between items-center"
>
<span className="font-medium">{item.title}</span>
<motion.span
animate={{ rotate: openIndex === index ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
▼
</motion.span>
</button>
<AnimatePresence>
{openIndex === index && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3 }}
>
<div className="p-4 pt-0 text-gray-600">
{item.content}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
))}
</div>
)
}Animated Counter
function AnimatedCounter({ value }: { value: number }) {
const count = useMotionValue(0)
const rounded = useTransform(count, (latest) => Math.round(latest))
const displayValue = useTransform(rounded, (latest) =>
latest.toLocaleString()
)
useEffect(() => {
const animation = animate(count, value, {
duration: 2,
ease: 'easeOut',
})
return animation.stop
}, [value, count])
return (
<motion.span className="text-4xl font-bold tabular-nums">
{displayValue}
</motion.span>
)
}Animated Toast
const toastVariants = {
initial: {
opacity: 0,
y: 50,
scale: 0.3,
},
animate: {
opacity: 1,
y: 0,
scale: 1,
transition: {
type: 'spring',
stiffness: 400,
damping: 25,
},
},
exit: {
opacity: 0,
x: 100,
transition: {
duration: 0.2,
},
},
}
function ToastContainer({ toasts, removeToast }) {
return (
<div className="fixed bottom-4 right-4 space-y-2 z-50">
<AnimatePresence mode="popLayout">
{toasts.map((toast) => (
<motion.div
key={toast.id}
layout
variants={toastVariants}
initial="initial"
animate="animate"
exit="exit"
className="bg-white shadow-lg rounded-lg p-4 min-w-[300px]"
>
<p>{toast.message}</p>
<button
onClick={() => removeToast(toast.id)}
className="absolute top-2 right-2"
>
×
</button>
</motion.div>
))}
</AnimatePresence>
</div>
)
}Performance Optimization
willChange
// Framer Motion automatically adds will-change
// But you can control it manually:
<motion.div
style={{ willChange: 'transform' }}
animate={{ x: 100 }}
/>Reduce Motion
import { useReducedMotion } from 'framer-motion'
function AccessibleAnimation() {
const shouldReduceMotion = useReducedMotion()
return (
<motion.div
animate={{
x: shouldReduceMotion ? 0 : 100,
opacity: 1,
}}
/>
)
}LazyMotion
import { LazyMotion, domAnimation, m } from 'framer-motion'
// Smaller bundle - loads only the features being used
function App() {
return (
<LazyMotion features={domAnimation}>
<m.div animate={{ opacity: 1 }} />
</LazyMotion>
)
}Framer Motion vs Alternatives
| Feature | Framer Motion | React Spring | GSAP |
|---|---|---|---|
| Bundle size | ~40KB | ~25KB | ~60KB |
| API | Declarative | Hook-based | Imperative |
| Exit animations | ✅ Native | ⚠️ Difficult | ⚠️ Manual |
| Layout animations | ✅ Native | ❌ | ⚠️ Plugin |
| Gestures | ✅ Built-in | ❌ | ⚠️ Plugin |
| SSR | ✅ | ✅ | ⚠️ |
| Learning curve | Easy | Medium | Hard |
FAQ
Does Framer Motion work with Next.js?
Yes! It supports SSR and App Router. Use 'use client' for components with animations.
How to animate during routing?
Use AnimatePresence with template.tsx in Next.js App Router.
Can I use it with TypeScript?
Yes, Framer Motion has full TypeScript types.
How to debug animations?
Use transition={{ duration: 2 }} to slow down animations. Check DevTools Performance.
Summary
Framer Motion is the best animation library for React:
- Simple API - animations in JSX
- Completeness - gestures, layout, exit animations
- Performance - optimizations out of the box
- TypeScript - full support
- Active development - regular updates
If you are building a React application and need animations - Framer Motion is the standard.