| name | animation-patterns |
| description | Framer Motion patterns, page transitions, skeleton loading, scroll-linked animations, and gesture-based interactions for React. |
Animation Patterns
Framer Motion patterns for production-quality React animations.
Framer Motion Basics (motion components, variants)
import { motion } from 'framer-motion'
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
/>
const cardVariants = {
hidden: { opacity: 0, y: 20, scale: 0.98 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { duration: 0.25, ease: [0.16, 1, 0.3, 1] },
},
hover: {
y: -4,
boxShadow: '0 12px 24px -4px rgb(0 0 0 / 0.15)',
transition: { duration: 0.2 },
},
}
function Card({ children }: { children: React.ReactNode }) {
return (
<motion.div
variants={cardVariants}
initial="hidden"
animate="visible"
whileHover="hover"
className="rounded-lg border bg-white p-4"
>
{children}
</motion.div>
)
}
Page Transitions with AnimatePresence
import { AnimatePresence, motion } from 'framer-motion'
import { usePathname } from 'next/navigation'
const pageVariants = {
initial: { opacity: 0, x: -8 },
enter: { opacity: 1, x: 0, transition: { duration: 0.2, ease: 'easeOut' } },
exit: { opacity: 0, x: 8, transition: { duration: 0.15, ease: 'easeIn' } },
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
return (
<html>
<body>
<AnimatePresence mode="wait">
<motion.main
key={pathname}
variants=
=
=
=
>
{children}
)
}
() {
(
)}
</>
)
}
Layout Animations (Shared Layout, Layout ID)
import { motion, LayoutGroup } from 'framer-motion'
import { useState } from 'react'
function AnimatedTabs({ tabs }: { tabs: string[] }) {
const [active, setActive] = useState(tabs[0])
return (
<LayoutGroup>
<div className="flex gap-1 rounded-lg bg-gray-100 p-1">
{tabs.map(tab => (
<button
key={tab}
onClick={() => setActive(tab)}
className="relative px-4 py-2 text-sm font-medium"
>
{active === tab && (
<motion.div
layoutId="tab-indicator" // shared across tabs
className="absolute inset-0 rounded-md bg-white shadow-sm"
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
/>
)}
<span className="relative z-10">{tab}</>
))}
)
}
() {
[expanded, setExpanded] = ()
(
)
}
Skeleton Loading Components
function Skeleton({ className }: { className?: string }) {
return (
<div
className={cn('animate-pulse rounded bg-gray-200 dark:bg-gray-700', className)}
aria-hidden="true"
/>
)
}
function SkeletonShimmer({ className }: { className?: string }) {
return (
<div className={cn('relative overflow-hidden rounded bg-gray-200', className)}>
<motion.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/60 to-transparent"
animate={{ x: ['-100%', '100%'] }}
transition={{ repeat: Infinity, duration: 1.2, ease: 'linear' }}
/>
</div>
)
}
function () {
(
)
}
Scroll-Linked Animations
import { useScroll, useTransform, motion } from 'framer-motion'
import { useRef } from 'react'
function ParallaxHero() {
const ref = useRef<HTMLDivElement>(null)
const { scrollYProgress } = useScroll({ target: ref, offset: ['start start', 'end start'] })
const y = useTransform(scrollYProgress, [0, 1], ['0%', '50%'])
const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0])
return (
<div ref={ref} className="relative h-screen overflow-hidden">
<motion.div style={{ y, opacity }} className="absolute inset-0">
<img src="/hero.jpg" alt="" className="w-full h-full object-cover" />
Hero Title
)
}
() {
ref = useRef<>()
{ scrollYProgress } = ({ : ref, : [, ] })
opacity = (scrollYProgress, [, ], [, ])
y = (scrollYProgress, [, ], [, ])
(
)
}
Staggered Children Animations
const listVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.06,
delayChildren: 0.1,
},
},
}
const itemVariants = {
hidden: { opacity: 0, x: -12 },
visible: { opacity: 1, x: 0, transition: { duration: 0.25, ease: 'easeOut' } },
}
function AnimatedList({ items }: { items: string[] }) {
return (
<motion.ul variants={listVariants} initial="hidden" animate="visible" className="space-y-2">
{items.map(item => (
<motion.li key={item} variants={itemVariants} className="rounded border p-3">
{item}
</motion.li>
))}
</motion.ul>
)
}
Gesture Interactions (Drag, Tap, Hover)
import { motion, useDragControls } from 'framer-motion'
function DraggableCard() {
return (
<motion.div
drag
dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
dragElastic={0.1}
whileDrag={{ scale: 1.05, rotate: 2, cursor: 'grabbing' }}
className="cursor-grab rounded-lg bg-white p-4 shadow"
>
Drag me
</motion.div>
)
}
function BottomSheet({ onClose }: { onClose: () => void }) {
return (
<motion.div
drag="y"
dragConstraints={{ top: 0 }}
onDragEnd={(_, info) => {
if (info.offset.y > 100) onClose()
}}
initial={{ y: '100%' }}
animate={{ y: 0 }}
exit={{ y: '100%' }}
transition={{ type: 'spring', damping: 30, stiffness: 300 }}
className="fixed bottom-0 inset-x-0 rounded-t-2xl bg-white p-6 shadow-lg"
>
{/* content */}
)
}
() {
(
)
}
Reduced Motion Respect
import { useReducedMotion, motion } from 'framer-motion'
function FadeIn({ children }: { children: React.ReactNode }) {
const prefersReducedMotion = useReducedMotion()
return (
<motion.div
initial={{ opacity: prefersReducedMotion ? 1 : 0 }}
animate={{ opacity: 1 }}
transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.3 }}
>
{children}
</motion.div>
)
}
import { MotionConfig } from 'framer-motion'
export function Providers({ children }: { children: React.ReactNode }) {
const prefersReducedMotion = useReducedMotion()
return (
<MotionConfig reducedMotion={prefersReducedMotion ? 'always' : 'never'}>
{children}
)
}
Performance Tips
<motion.div style={{ willChange: 'transform' }} />
<motion.div layout />
<motion.div layout="position"> // layout="position": cheaper, only position
// 4. LazyMotion — reduces bundle size (removes unused features)
import { LazyMotion, domAnimation, m } from 'framer-motion'
<LazyMotion features={domAnimation}> {/* load only DOM animations */}
<m.div animate={{ opacity: 1 }} /> {/* use m instead of motion */}
</LazyMotion>
// 5. Avoid animating inside virtualized lists
// AnimatePresence + virtualization = bugs. Use CSS transitions for list items.
// 6. Spring physics vs duration — springs feel more natural
const spring = { type: 'spring', stiffness: 300, damping: 25 }
const duration = { duration: 0.3, ease: [0.16, 1, 0.3, 1] }
// Springs: interactive elements (drag release, hover)
// Duration: page transitions, content reveal