Implementing Framer Motion Animations in React Applications
Framer Motion is a declarative animation library for React. Unlike GSAP, it's integrated into React's lifecycle: mount/unmount animations work natively, animation state is synchronized with component state. This makes it the preferred choice for React applications where animations are closely tied to UI logic.
Installation
npm install framer-motion
For Next.js 13+ with App Router: components with motion should be "use client". Framer Motion doesn't work in RSC.
Basic Patterns
Motion Components and Variants
Variants are a declarative way to describe animation states:
// components/AnimatedCard.tsx
'use client'
import { motion, Variants } from 'framer-motion'
const cardVariants: Variants = {
hidden: {
opacity: 0,
y: 30,
scale: 0.97,
},
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: {
duration: 0.5,
ease: [0.25, 0.46, 0.45, 0.94], // custom cubic-bezier
},
},
hover: {
y: -4,
boxShadow: '0 20px 40px rgba(0,0,0,0.12)',
transition: { duration: 0.2, ease: 'easeOut' },
},
tap: {
scale: 0.98,
transition: { duration: 0.1 },
},
}
interface AnimatedCardProps {
children: React.ReactNode
delay?: number
}
export function AnimatedCard({ children, delay = 0 }: AnimatedCardProps) {
return (
<motion.div
variants={cardVariants}
initial="hidden"
animate="visible"
whileHover="hover"
whileTap="tap"
transition={{ delay }}
className="bg-white rounded-xl p-6 shadow-md cursor-pointer"
>
{children}
</motion.div>
)
}
Stagger Animation of Child Elements
Framer Motion automatically passes variants to child components through context — no need to pass the variant explicitly:
// components/AnimatedList.tsx
'use client'
import { motion, Variants } from 'framer-motion'
const containerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.08, // delay between children
delayChildren: 0.2,
},
},
}
const itemVariants: Variants = {
hidden: { opacity: 0, x: -20 },
visible: {
opacity: 1,
x: 0,
transition: { duration: 0.4, ease: 'easeOut' },
},
}
export function AnimatedList({ items }: { items: string[] }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
{items.map((item, i) => (
<motion.li key={i} variants={itemVariants}>
{item}
</motion.li>
))}
</motion.ul>
)
}
AnimatePresence: Unmount Animation
Standard React doesn't allow animating components on unmount — they simply disappear. AnimatePresence solves this:
// components/Modal.tsx
'use client'
import { AnimatePresence, motion } from 'framer-motion'
interface ModalProps {
isOpen: boolean
onClose: () => void
children: React.ReactNode
}
const overlayVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1 },
}
const modalVariants = {
hidden: { opacity: 0, scale: 0.95, y: -20 },
visible: {
opacity: 1,
scale: 1,
y: 0,
transition: { type: 'spring', stiffness: 300, damping: 30 },
},
exit: {
opacity: 0,
scale: 0.95,
y: -10,
transition: { duration: 0.15 },
},
}
export function Modal({ isOpen, onClose, children }: ModalProps) {
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div
key="overlay"
className="fixed inset-0 bg-black/50 z-40"
variants={overlayVariants}
initial="hidden"
animate="visible"
exit="hidden"
onClick={onClose}
/>
<motion.div
key="modal"
className="fixed inset-0 flex items-center justify-center z-50 pointer-events-none"
>
<motion.div
className="bg-white rounded-2xl p-8 max-w-lg w-full mx-4 pointer-events-auto"
variants={modalVariants}
initial="hidden"
animate="visible"
exit="exit"
>
{children}
</motion.div>
</motion.div>
</>
)}
</AnimatePresence>
)
}
useMotionValue and useTransform
For animations tied to mouse movement or scroll:
// components/MagneticButton.tsx
'use client'
import { useRef, useState } from 'react'
import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion'
export function MagneticButton({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLButtonElement>(null)
const x = useMotionValue(0)
const y = useMotionValue(0)
// Spring physics for smoothness
const springConfig = { stiffness: 200, damping: 20 }
const springX = useSpring(x, springConfig)
const springY = useSpring(y, springConfig)
const handleMouseMove = (e: React.MouseEvent) => {
const rect = ref.current!.getBoundingClientRect()
const cx = rect.left + rect.width / 2
const cy = rect.top + rect.height / 2
const strength = 0.3
x.set((e.clientX - cx) * strength)
y.set((e.clientY - cy) * strength)
}
const handleMouseLeave = () => {
x.set(0)
y.set(0)
}
return (
<motion.button
ref={ref}
style={{ x: springX, y: springY }}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className="px-6 py-3 bg-black text-white rounded-full font-medium"
>
{children}
</motion.button>
)
}
Scroll Animations via useScroll
// components/ProgressBar.tsx
'use client'
import { useScroll, useSpring, motion } from 'framer-motion'
export function ReadingProgressBar() {
const { scrollYProgress } = useScroll()
const scaleX = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001,
})
return (
<motion.div
style={{ scaleX, transformOrigin: '0%' }}
className="fixed top-0 left-0 right-0 h-1 bg-blue-500 z-50"
/>
)
}
Section animation on scroll with whileInView:
// components/FadeInSection.tsx
'use client'
import { motion } from 'framer-motion'
export function FadeInSection({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-100px' }}
transition={{ duration: 0.6, ease: 'easeOut' }}
>
{children}
</motion.div>
)
}
viewport.once: true — animation triggers only once. margin: '-100px' — starts animation 100px before entering viewport.
Shared Layout Animations (layoutId)
Smooth transitions between elements via layoutId — Framer Motion tracks DOM position and interpolates:
// components/TabsWithAnimation.tsx
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
const tabs = ['Overview', 'Features', 'Pricing']
export function AnimatedTabs() {
const [active, setActive] = useState(0)
return (
<div className="flex gap-1 bg-gray-100 p-1 rounded-lg w-fit">
{tabs.map((tab, i) => (
<button
key={tab}
onClick={() => setActive(i)}
className="relative px-4 py-2 text-sm font-medium z-10"
>
{active === i && (
<motion.div
layoutId="active-tab" // unique ID for tracking
className="absolute inset-0 bg-white rounded-md shadow-sm"
transition={{ type: 'spring', stiffness: 400, damping: 35 }}
/>
)}
<span className="relative">{tab}</span>
</button>
))}
</div>
)
}
Typical Timelines
Basic animation set (fade-in, stagger, hover) — 1 working day. AnimatePresence for modals/drawers/routing + layout animations + scroll effects — 3–4 working days. Complex interactive scenes with useMotionValue and spring physics — from 5 days.







