---
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)
```typescript
// Install: npm install framer-motion
import { motion } from 'framer-motion'
// Basic motion component
// Variants — define states, animate by name
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 (
{children}
)
}
```
## Page Transitions with AnimatePresence
```typescript
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' } },
}
// app/layout.tsx (Next.js App Router)
export default function RootLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
return (
{children}
)
}
// Modal transitions — mount/unmount
function Modal({ isOpen, onClose, children }: ModalProps) {
return (
{isOpen && (
<>
{children}
>
)}
)
}
```
## Layout Animations (Shared Layout, Layout ID)
```typescript
import { motion, LayoutGroup } from 'framer-motion'
import { useState } from 'react'
// Tabs with animated indicator
function AnimatedTabs({ tabs }: { tabs: string[] }) {
const [active, setActive] = useState(tabs[0])
return (
{tabs.map(tab => (
))}
)
}
// Expandable card with layout animation
function ExpandableCard({ title, body }: { title: string; body: string }) {
const [expanded, setExpanded] = useState(false)
return (
setExpanded(e => !e)}
>
{title}
{expanded && (
{body}
)}
)
}
```
## Skeleton Loading Components
```typescript
// Base skeleton primitive
function Skeleton({ className }: { className?: string }) {
return (
)
}
// Shimmer variant (more polished)
function SkeletonShimmer({ className }: { className?: string }) {
return (
)
}
// Card skeleton
function CardSkeleton() {
return (
)
}
```
## Scroll-Linked Animations
```typescript
import { useScroll, useTransform, motion } from 'framer-motion'
import { useRef } from 'react'
// Parallax hero
function ParallaxHero() {
const ref = useRef(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 (
Hero Title
)
}
// Fade-in on scroll (section reveal)
function FadeInSection({ children }: { children: React.ReactNode }) {
const ref = useRef(null)
const { scrollYProgress } = useScroll({ target: ref, offset: ['start 0.9', 'start 0.6'] })
const opacity = useTransform(scrollYProgress, [0, 1], [0, 1])
const y = useTransform(scrollYProgress, [0, 1], [24, 0])
return (
{children}
)
}
```
## Staggered Children Animations
```typescript
const listVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.06, // 60ms between each child
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 (
{items.map(item => (
{item}
))}
)
}
```
## Gesture Interactions (Drag, Tap, Hover)
```typescript
import { motion, useDragControls } from 'framer-motion'
// Draggable card
function DraggableCard() {
return (
Drag me
)
}
// Bottom sheet (drag to dismiss)
function BottomSheet({ onClose }: { onClose: () => void }) {
return (
{
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 */}
)
}
// Press / tap feedback
function PressButton({ children, onClick }: ButtonProps) {
return (
{children}
)
}
```
## Reduced Motion Respect
```typescript
import { useReducedMotion, motion } from 'framer-motion'
// Hook-based — apply conditionally
function FadeIn({ children }: { children: React.ReactNode }) {
const prefersReducedMotion = useReducedMotion()
return (
{children}
)
}
// Global — wrap app with MotionConfig
import { MotionConfig } from 'framer-motion'
export function Providers({ children }: { children: React.ReactNode }) {
const prefersReducedMotion = useReducedMotion()
return (
{children}
)
}
// CSS fallback (always include alongside JS animations)
// @media (prefers-reduced-motion: reduce) { .animated { animation: none !important; } }
```
## Performance Tips
```typescript
// 1. Use transform properties — GPU accelerated
// GOOD: x, y, scale, rotate, opacity (compositor layer)
// BAD: width, height, top, left, margin (layout thrash)
// 2. will-change for known animations (use sparingly)
// 3. layout prop only when needed — triggers LayoutAnimation (expensive)
// layout: expensive, triggers every render
// layout="position": cheaper, only position
// 4. LazyMotion — reduces bundle size (removes unused features)
import { LazyMotion, domAnimation, m } from 'framer-motion'
{/* load only DOM animations */}
{/* use m instead of motion */}
// 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
```