
Framer Motion
- 15 installs
- 404 repo stars
- Updated August 5, 2026
- aiskillstore/marketplace
framer-motion is a Claude skill covering Framer Motion animation patterns for React, including motion components, variants, gestures, page transitions, and scroll animations.
About
This skill covers Framer Motion animations for React applications. It provides motion component usage, variants, gestures, page and list transitions, scroll-triggered animations, transition and easing options, reduced-motion handling, and reusable templates. A developer uses it when adding animations to React or Next.js interfaces.
- Framer Motion animation patterns for React and Next.js applications
- Covers motion components, variants, gestures, page transitions, and scroll animations
- Includes reduced-motion handling and ready-to-use component templates
Framer Motion by the numbers
- 15 all-time installs (skills.sh)
- Ranked #1,610 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
framer-motion capabilities & compatibility
Free; installs the open-source framer-motion npm package.
- Capabilities
- react animation · page transitions · scroll animation · gesture handling
- Use cases
- frontend · ui design
- IDEs
- vscode · cursor ide
- Runs
- Runs locally
- Pricing
- Free
What framer-motion says it does
Comprehensive Framer Motion animation library for React.
Always respect user preferences:
npx skills add https://github.com/aiskillstore/marketplace --skill framer-motionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 15 |
|---|---|
| repo stars | ★ 404 |
| Last updated | August 5, 2026 |
| Repository | aiskillstore/marketplace ↗ |
What it does
Add Framer Motion animations, transitions, and gestures to React and Next.js interfaces.
When should I use this skill?
When adding animations to React or Next.js applications.
What you get
React components animated with Framer Motion, respecting reduced-motion preferences.
- animated React components
- page transition wrapper
- animated list component
By the numbers
- 4 reference guides (motion component, variants, gestures, hooks)
- 4 example patterns
- 2 component templates
Files
Framer Motion Skill
Production-ready animations for React applications.
Quick Start
Installation
npm install framer-motion
# or
pnpm add framer-motionBasic Usage
import { motion } from "framer-motion";
// Simple animation
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
Content
</motion.div>Core Concepts
| Concept | Guide |
|---|---|
| Motion Component | reference/motion-component.md |
| Variants | reference/variants.md |
| Gestures | reference/gestures.md |
| Hooks | reference/hooks.md |
Examples
| Pattern | Guide |
|---|---|
| Page Transitions | examples/page-transitions.md |
| List Animations | examples/list-animations.md |
| Scroll Animations | examples/scroll-animations.md |
| Micro-interactions | examples/micro-interactions.md |
Templates
| Template | Purpose |
|---|---|
| templates/page-transition.tsx | Page transition wrapper |
| templates/animated-list.tsx | Animated list component |
Quick Reference
Basic Animation
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
Content
</motion.div>Hover & Tap
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
Click me
</motion.button>Variants
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.1 }
}
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
};
<motion.ul variants={container} initial="hidden" animate="show">
{items.map(i => (
<motion.li key={i} variants={item}>{i}</motion.li>
))}
</motion.ul>AnimatePresence (Exit Animations)
import { AnimatePresence, motion } from "framer-motion";
<AnimatePresence mode="wait">
{isVisible && (
<motion.div
key="modal"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
Modal content
</motion.div>
)}
</AnimatePresence>Scroll Trigger
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5 }}
>
Animates when scrolled into view
</motion.div>Drag
<motion.div
drag
dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }}
dragElastic={0.1}
>
Drag me
</motion.div>Layout Animation
<motion.div layout layoutId="shared-element">
Content that animates when layout changes
</motion.div>Transition Types
// Tween (default)
transition={{ duration: 0.3, ease: "easeOut" }}
// Spring
transition={{ type: "spring", stiffness: 300, damping: 20 }}
// Spring presets
transition={{ type: "spring", bounce: 0.25 }}
// Inertia (for drag)
transition={{ type: "inertia", velocity: 50 }}Easing Functions
// Built-in easings
ease: "linear"
ease: "easeIn"
ease: "easeOut"
ease: "easeInOut"
ease: "circIn"
ease: "circOut"
ease: "circInOut"
ease: "backIn"
ease: "backOut"
ease: "backInOut"
// Custom cubic-bezier
ease: [0.17, 0.67, 0.83, 0.67]Reduced Motion
Always respect user preferences:
import { motion, useReducedMotion } from "framer-motion";
function Component() {
const prefersReducedMotion = useReducedMotion();
return (
<motion.div
initial={{ opacity: 0, y: prefersReducedMotion ? 0 : 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: prefersReducedMotion ? 0 : 0.3 }}
>
Respects motion preferences
</motion.div>
);
}
// Or use media query
const variants = {
initial: { opacity: 0 },
animate: { opacity: 1 },
};
<motion.div
variants={variants}
initial="initial"
animate="animate"
className="motion-reduce:transition-none"
>Common Patterns
Fade In Up
const fadeInUp = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.4 }
};
<motion.div {...fadeInUp}>Content</motion.div>Staggered List
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.1, delayChildren: 0.2 }
}
};
const item = {
hidden: { opacity: 0, x: -20 },
show: { opacity: 1, x: 0 }
};Modal
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50"
onClick={onClose}
/>
{/* Modal */}
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="fixed inset-x-4 top-1/2 -translate-y-1/2 ..."
>
Modal content
</motion.div>
</>
)}
</AnimatePresence>Accordion
<motion.div
initial={false}
animate={{ height: isOpen ? "auto" : 0 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
className="overflow-hidden"
>
<div className="p-4">Accordion content</div>
</motion.div>Best Practices
1. Use variants: Cleaner code, easier orchestration 2. Respect reduced motion: Always check useReducedMotion 3. Use `layout` sparingly: Can be expensive, use only when needed 4. Exit animations: Wrap with AnimatePresence 5. Spring for interactions: More natural feel for hover/tap 6. Tween for page transitions: More predictable timing 7. GPU-accelerated properties: Prefer opacity, scale, x, y over width, height
List Animation Examples
Animated lists, staggered items, and reorderable lists.
Basic Staggered List
"use client";
import { motion } from "framer-motion";
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.2,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
type: "spring",
stiffness: 300,
damping: 24,
},
},
};
export function StaggeredList({ items }: { items: string[] }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-2"
>
{items.map((item, index) => (
<motion.li
key={index}
variants={itemVariants}
className="p-4 bg-card rounded-lg border"
>
{item}
</motion.li>
))}
</motion.ul>
);
}List with Entry and Exit Animations
"use client";
import { AnimatePresence, motion } from "framer-motion";
interface Item {
id: string;
text: string;
}
const itemVariants = {
initial: { opacity: 0, height: 0, y: -10 },
animate: {
opacity: 1,
height: "auto",
y: 0,
transition: {
type: "spring",
stiffness: 300,
damping: 24,
},
},
exit: {
opacity: 0,
height: 0,
y: -10,
transition: {
duration: 0.2,
},
},
};
export function AnimatedList({ items }: { items: Item[] }) {
return (
<ul className="space-y-2">
<AnimatePresence mode="popLayout">
{items.map((item) => (
<motion.li
key={item.id}
layout
variants={itemVariants}
initial="initial"
animate="animate"
exit="exit"
className="p-4 bg-card rounded-lg border"
>
{item.text}
</motion.li>
))}
</AnimatePresence>
</ul>
);
}Todo List with Add/Remove
"use client";
import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Plus, X } from "lucide-react";
interface Todo {
id: string;
text: string;
completed: boolean;
}
export function AnimatedTodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const [newTodo, setNewTodo] = useState("");
function addTodo() {
if (!newTodo.trim()) return;
setTodos([
...todos,
{ id: crypto.randomUUID(), text: newTodo, completed: false },
]);
setNewTodo("");
}
function removeTodo(id: string) {
setTodos(todos.filter((t) => t.id !== id));
}
function toggleTodo(id: string) {
setTodos(
todos.map((t) =>
t.id === id ? { ...t, completed: !t.completed } : t
)
);
}
return (
<div className="space-y-4">
<div className="flex gap-2">
<input
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && addTodo()}
placeholder="Add todo..."
className="flex-1 px-3 py-2 border rounded-lg"
/>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={addTodo}
className="p-2 bg-primary text-primary-foreground rounded-lg"
>
<Plus className="h-5 w-5" />
</motion.button>
</div>
<ul className="space-y-2">
<AnimatePresence mode="popLayout">
{todos.map((todo) => (
<motion.li
key={todo.id}
layout
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8, transition: { duration: 0.2 } }}
className="flex items-center gap-3 p-4 bg-card rounded-lg border"
>
<motion.input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
whileTap={{ scale: 0.9 }}
/>
<motion.span
animate={{
opacity: todo.completed ? 0.5 : 1,
textDecoration: todo.completed ? "line-through" : "none",
}}
className="flex-1"
>
{todo.text}
</motion.span>
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={() => removeTodo(todo.id)}
className="p-1 text-destructive"
>
<X className="h-4 w-4" />
</motion.button>
</motion.li>
))}
</AnimatePresence>
</ul>
</div>
);
}Reorderable List (Drag to Reorder)
"use client";
import { useState } from "react";
import { Reorder } from "framer-motion";
import { GripVertical } from "lucide-react";
interface Item {
id: string;
name: string;
}
export function ReorderableList({ initialItems }: { initialItems: Item[] }) {
const [items, setItems] = useState(initialItems);
return (
<Reorder.Group
axis="y"
values={items}
onReorder={setItems}
className="space-y-2"
>
{items.map((item) => (
<Reorder.Item
key={item.id}
value={item}
className="flex items-center gap-3 p-4 bg-card rounded-lg border cursor-grab active:cursor-grabbing"
>
<GripVertical className="h-5 w-5 text-muted-foreground" />
<span>{item.name}</span>
</Reorder.Item>
))}
</Reorder.Group>
);
}Reorderable with Custom Handle
"use client";
import { useState } from "react";
import { Reorder, useDragControls } from "framer-motion";
import { GripVertical, X } from "lucide-react";
interface Item {
id: string;
name: string;
}
function ReorderItem({
item,
onRemove,
}: {
item: Item;
onRemove: (id: string) => void;
}) {
const dragControls = useDragControls();
return (
<Reorder.Item
value={item}
dragControls={dragControls}
dragListener={false}
className="flex items-center gap-3 p-4 bg-card rounded-lg border"
>
{/* Drag handle */}
<div
onPointerDown={(e) => dragControls.start(e)}
className="cursor-grab active:cursor-grabbing p-1 -m-1"
>
<GripVertical className="h-5 w-5 text-muted-foreground" />
</div>
{/* Content */}
<span className="flex-1">{item.name}</span>
{/* Remove button */}
<button
onClick={() => onRemove(item.id)}
className="p-1 text-muted-foreground hover:text-destructive"
>
<X className="h-4 w-4" />
</button>
</Reorder.Item>
);
}
export function ReorderableWithHandle({ initialItems }: { initialItems: Item[] }) {
const [items, setItems] = useState(initialItems);
function removeItem(id: string) {
setItems(items.filter((item) => item.id !== id));
}
return (
<Reorder.Group axis="y" values={items} onReorder={setItems} className="space-y-2">
{items.map((item) => (
<ReorderItem key={item.id} item={item} onRemove={removeItem} />
))}
</Reorder.Group>
);
}Grid Layout Animation
"use client";
import { motion } from "framer-motion";
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.05,
},
},
};
const itemVariants = {
hidden: { opacity: 0, scale: 0.8 },
visible: {
opacity: 1,
scale: 1,
transition: {
type: "spring",
stiffness: 300,
damping: 24,
},
},
};
export function AnimatedGrid({ items }: { items: any[] }) {
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"
>
{items.map((item) => (
<motion.div
key={item.id}
variants={itemVariants}
whileHover={{ y: -5, boxShadow: "0 10px 30px -10px rgba(0,0,0,0.2)" }}
className="p-6 bg-card rounded-xl border"
>
{item.content}
</motion.div>
))}
</motion.div>
);
}Filterable List
"use client";
import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
interface Item {
id: string;
name: string;
category: string;
}
export function FilterableList({ items }: { items: Item[] }) {
const [filter, setFilter] = useState<string | null>(null);
const categories = [...new Set(items.map((item) => item.category))];
const filteredItems = filter
? items.filter((item) => item.category === filter)
: items;
return (
<div className="space-y-4">
{/* Filter buttons */}
<div className="flex gap-2">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setFilter(null)}
className={`px-4 py-2 rounded-lg ${
filter === null ? "bg-primary text-primary-foreground" : "bg-muted"
}`}
>
All
</motion.button>
{categories.map((category) => (
<motion.button
key={category}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setFilter(category)}
className={`px-4 py-2 rounded-lg ${
filter === category
? "bg-primary text-primary-foreground"
: "bg-muted"
}`}
>
{category}
</motion.button>
))}
</div>
{/* List */}
<motion.div layout className="grid grid-cols-2 lg:grid-cols-3 gap-4">
<AnimatePresence mode="popLayout">
{filteredItems.map((item) => (
<motion.div
key={item.id}
layout
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.3 }}
className="p-4 bg-card rounded-lg border"
>
<p className="font-medium">{item.name}</p>
<p className="text-sm text-muted-foreground">{item.category}</p>
</motion.div>
))}
</AnimatePresence>
</motion.div>
</div>
);
}Infinite Scroll List
"use client";
import { useRef, useState } from "react";
import { motion, useInView } from "framer-motion";
export function InfiniteScrollList() {
const [items, setItems] = useState(Array.from({ length: 10 }, (_, i) => i));
const loadMoreRef = useRef(null);
const isInView = useInView(loadMoreRef);
// Load more when sentinel comes into view
React.useEffect(() => {
if (isInView) {
setItems((prev) => [
...prev,
...Array.from({ length: 10 }, (_, i) => prev.length + i),
]);
}
}, [isInView]);
return (
<div className="space-y-2">
{items.map((item, index) => (
<motion.div
key={item}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: (index % 10) * 0.05 }}
className="p-4 bg-card rounded-lg border"
>
Item {item}
</motion.div>
))}
{/* Load more trigger */}
<div ref={loadMoreRef} className="h-10 flex items-center justify-center">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
className="w-6 h-6 border-2 border-primary border-t-transparent rounded-full"
/>
</div>
</div>
);
}Best Practices
1. Use `layout` prop: For smooth position transitions when items change 2. Use `mode="popLayout"`: Prevents layout jumps during exit animations 3. Keep items keyed: Always use unique, stable keys for list items 4. Stagger subtly: 0.05-0.1s between items is usually enough 5. Spring for snappy: Use spring animations for interactive lists 6. Exit animations: Keep exit animations shorter than enter (0.2s vs 0.3s)
Micro-interaction Examples
Small, delightful animations that enhance UI interactions.
Button Interactions
Basic Button
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
className="px-6 py-3 bg-primary text-primary-foreground rounded-lg"
>
Click me
</motion.button>Button with Icon Animation
"use client";
import { motion } from "framer-motion";
import { ArrowRight } from "lucide-react";
export function ButtonWithArrow() {
return (
<motion.button
whileHover="hover"
className="group flex items-center gap-2 px-6 py-3 bg-primary text-primary-foreground rounded-lg"
>
<span>Continue</span>
<motion.span
variants={{
hover: { x: 5 },
}}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<ArrowRight className="h-4 w-4" />
</motion.span>
</motion.button>
);
}Loading Button
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { Loader2, Check } from "lucide-react";
type ButtonState = "idle" | "loading" | "success";
export function LoadingButton({
state,
onClick,
}: {
state: ButtonState;
onClick: () => void;
}) {
return (
<motion.button
onClick={onClick}
disabled={state !== "idle"}
whileHover={state === "idle" ? { scale: 1.02 } : {}}
whileTap={state === "idle" ? { scale: 0.98 } : {}}
className="relative px-6 py-3 bg-primary text-primary-foreground rounded-lg overflow-hidden min-w-[120px]"
>
<AnimatePresence mode="wait">
{state === "idle" && (
<motion.span
key="idle"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
>
Submit
</motion.span>
)}
{state === "loading" && (
<motion.span
key="loading"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex items-center justify-center"
>
<Loader2 className="h-5 w-5 animate-spin" />
</motion.span>
)}
{state === "success" && (
<motion.span
key="success"
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
className="flex items-center justify-center"
>
<Check className="h-5 w-5" />
</motion.span>
)}
</AnimatePresence>
</motion.button>
);
}Card Interactions
Hover Lift Card
<motion.div
whileHover={{
y: -5,
boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)",
}}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
className="p-6 bg-card rounded-xl border"
>
Card content
</motion.div>Card with Glow Effect
"use client";
import { motion, useMotionTemplate, useMotionValue } from "framer-motion";
export function GlowCard({ children }: { children: React.ReactNode }) {
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
function handleMouseMove(e: React.MouseEvent) {
const { left, top } = e.currentTarget.getBoundingClientRect();
mouseX.set(e.clientX - left);
mouseY.set(e.clientY - top);
}
const background = useMotionTemplate`radial-gradient(
200px circle at ${mouseX}px ${mouseY}px,
rgba(59, 130, 246, 0.15),
transparent 80%
)`;
return (
<motion.div
onMouseMove={handleMouseMove}
style={{ background }}
whileHover={{ scale: 1.02 }}
className="relative p-6 bg-card rounded-xl border overflow-hidden"
>
{children}
</motion.div>
);
}Expandable Card
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronDown } from "lucide-react";
export function ExpandableCard({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="border rounded-xl overflow-hidden">
<motion.button
onClick={() => setIsOpen(!isOpen)}
className="w-full flex items-center justify-between p-4 text-left"
whileHover={{ backgroundColor: "rgba(0,0,0,0.02)" }}
>
<span className="font-medium">{title}</span>
<motion.span animate={{ rotate: isOpen ? 180 : 0 }}>
<ChevronDown className="h-5 w-5" />
</motion.span>
</motion.button>
<AnimatePresence initial={false}>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
className="overflow-hidden"
>
<div className="p-4 pt-0 border-t">{children}</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}Input Interactions
Floating Label Input
"use client";
import { useState } from "react";
import { motion } from "framer-motion";
export function FloatingLabelInput({ label }: { label: string }) {
const [isFocused, setIsFocused] = useState(false);
const [value, setValue] = useState("");
const isActive = isFocused || value.length > 0;
return (
<div className="relative">
<motion.label
initial={false}
animate={{
y: isActive ? -24 : 0,
scale: isActive ? 0.85 : 1,
color: isFocused ? "hsl(var(--primary))" : "hsl(var(--muted-foreground))",
}}
className="absolute left-3 top-3 origin-left pointer-events-none"
>
{label}
</motion.label>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
className="w-full px-3 py-3 border rounded-lg focus:ring-2 focus:ring-primary outline-none"
/>
</div>
);
}Search Input with Icon
"use client";
import { motion } from "framer-motion";
import { Search, X } from "lucide-react";
export function SearchInput({
value,
onChange,
onClear,
}: {
value: string;
onChange: (value: string) => void;
onClear: () => void;
}) {
return (
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<input
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder="Search..."
className="w-full pl-10 pr-10 py-2 border rounded-lg focus:ring-2 focus:ring-primary outline-none"
/>
<AnimatePresence>
{value && (
<motion.button
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
onClick={onClear}
className="absolute right-3 top-1/2 -translate-y-1/2"
>
<X className="h-4 w-4 text-muted-foreground" />
</motion.button>
)}
</AnimatePresence>
</div>
);
}Toggle & Switch
Animated Toggle
"use client";
import { motion } from "framer-motion";
export function AnimatedToggle({
isOn,
onToggle,
}: {
isOn: boolean;
onToggle: () => void;
}) {
return (
<motion.button
onClick={onToggle}
animate={{ backgroundColor: isOn ? "hsl(var(--primary))" : "hsl(var(--muted))" }}
className="w-14 h-8 rounded-full p-1"
>
<motion.div
animate={{ x: isOn ? 24 : 0 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
className="w-6 h-6 bg-white rounded-full shadow-md"
/>
</motion.button>
);
}Modal Interactions
Modal with Backdrop
"use client";
import { AnimatePresence, motion } from "framer-motion";
import { X } from "lucide-react";
export function AnimatedModal({
isOpen,
onClose,
children,
}: {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}) {
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="fixed inset-0 bg-black/50 z-40"
/>
{/* Modal */}
<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={{ type: "spring", damping: 25, stiffness: 300 }}
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 w-full max-w-md bg-background rounded-xl p-6 shadow-xl"
>
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={onClose}
className="absolute top-4 right-4"
>
<X className="h-5 w-5" />
</motion.button>
{children}
</motion.div>
</>
)}
</AnimatePresence>
);
}Notification Toast
"use client";
import { AnimatePresence, motion } from "framer-motion";
import { CheckCircle, X } from "lucide-react";
export function AnimatedToast({
isVisible,
message,
onClose,
}: {
isVisible: boolean;
message: string;
onClose: () => void;
}) {
return (
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0, y: 50, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.9 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="fixed bottom-4 right-4 flex items-center gap-3 px-4 py-3 bg-green-500 text-white rounded-lg shadow-lg"
>
<CheckCircle className="h-5 w-5" />
<span>{message}</span>
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={onClose}
>
<X className="h-4 w-4" />
</motion.button>
</motion.div>
)}
</AnimatePresence>
);
}Loading Spinner
"use client";
import { motion } from "framer-motion";
export function LoadingSpinner() {
return (
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
className="w-6 h-6 border-2 border-primary border-t-transparent rounded-full"
/>
);
}
// Pulsing dots
export function LoadingDots() {
return (
<div className="flex gap-1">
{[0, 1, 2].map((i) => (
<motion.div
key={i}
animate={{ scale: [1, 1.2, 1] }}
transition={{
duration: 0.6,
repeat: Infinity,
delay: i * 0.2,
}}
className="w-2 h-2 bg-primary rounded-full"
/>
))}
</div>
);
}Checkbox Animation
"use client";
import { motion } from "framer-motion";
import { Check } from "lucide-react";
export function AnimatedCheckbox({
checked,
onChange,
}: {
checked: boolean;
onChange: (checked: boolean) => void;
}) {
return (
<motion.button
onClick={() => onChange(!checked)}
animate={{
backgroundColor: checked ? "hsl(var(--primary))" : "transparent",
borderColor: checked ? "hsl(var(--primary))" : "hsl(var(--border))",
}}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="w-5 h-5 border-2 rounded flex items-center justify-center"
>
<motion.span
initial={false}
animate={{ scale: checked ? 1 : 0 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
>
<Check className="h-3 w-3 text-primary-foreground" />
</motion.span>
</motion.button>
);
}Best Practices
1. Keep it subtle: Micro-interactions should enhance, not distract 2. Use springs for responsiveness: They feel more natural than tweens 3. Short durations: 100-300ms for most micro-interactions 4. Consistent timing: Use the same spring settings throughout your app 5. Purpose over decoration: Every animation should have a reason 6. Test without animations: UI should work without motion
Page Transition Examples
Smooth transitions between pages and routes.
Basic Page Transition (Next.js App Router)
Page Wrapper Component
// components/page-transition.tsx
"use client";
import { motion } from "framer-motion";
import { ReactNode } from "react";
const pageVariants = {
initial: {
opacity: 0,
},
enter: {
opacity: 1,
transition: {
duration: 0.3,
ease: "easeOut",
},
},
exit: {
opacity: 0,
transition: {
duration: 0.2,
ease: "easeIn",
},
},
};
interface PageTransitionProps {
children: ReactNode;
}
export function PageTransition({ children }: PageTransitionProps) {
return (
<motion.div
variants={pageVariants}
initial="initial"
animate="enter"
exit="exit"
>
{children}
</motion.div>
);
}
// Usage in page
// app/about/page.tsx
import { PageTransition } from "@/components/page-transition";
export default function AboutPage() {
return (
<PageTransition>
<h1>About</h1>
<p>Page content here...</p>
</PageTransition>
);
}Slide Transitions
Slide from Right
const slideRightVariants = {
initial: {
opacity: 0,
x: 20,
},
enter: {
opacity: 1,
x: 0,
transition: {
duration: 0.4,
ease: [0.25, 0.1, 0.25, 1], // Custom cubic-bezier
},
},
exit: {
opacity: 0,
x: -20,
transition: {
duration: 0.3,
},
},
};Slide from Bottom
const slideUpVariants = {
initial: {
opacity: 0,
y: 30,
},
enter: {
opacity: 1,
y: 0,
transition: {
duration: 0.4,
ease: "easeOut",
},
},
exit: {
opacity: 0,
y: -20,
transition: {
duration: 0.3,
},
},
};Slide with Scale
const slideScaleVariants = {
initial: {
opacity: 0,
y: 20,
scale: 0.98,
},
enter: {
opacity: 1,
y: 0,
scale: 1,
transition: {
duration: 0.4,
ease: [0.25, 0.1, 0.25, 1],
},
},
exit: {
opacity: 0,
scale: 0.98,
transition: {
duration: 0.3,
},
},
};Staggered Page Content
const pageVariants = {
initial: {
opacity: 0,
},
enter: {
opacity: 1,
transition: {
duration: 0.3,
when: "beforeChildren",
staggerChildren: 0.1,
},
},
};
const itemVariants = {
initial: {
opacity: 0,
y: 20,
},
enter: {
opacity: 1,
y: 0,
transition: {
duration: 0.4,
},
},
};
export function StaggeredPage({ children }) {
return (
<motion.div
variants={pageVariants}
initial="initial"
animate="enter"
>
<motion.h1 variants={itemVariants}>Page Title</motion.h1>
<motion.p variants={itemVariants}>Description</motion.p>
<motion.div variants={itemVariants}>{children}</motion.div>
</motion.div>
);
}AnimatePresence for Route Changes
Template Component (App Router)
// app/template.tsx
"use client";
import { AnimatePresence, motion } 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>
);
}Mode Options
// mode="wait" - Wait for exit animation before entering
<AnimatePresence mode="wait">
{/* Only one child visible at a time */}
</AnimatePresence>
// mode="sync" - Enter and exit simultaneously (default)
<AnimatePresence mode="sync">
{/* Both visible during transition */}
</AnimatePresence>
// mode="popLayout" - For layout animations
<AnimatePresence mode="popLayout">
{/* Maintains layout during exit */}
</AnimatePresence>Shared Element Transitions
// components/card.tsx
"use client";
import { motion } from "framer-motion";
import Link from "next/link";
interface CardProps {
id: string;
title: string;
image: string;
}
export function Card({ id, title, image }: CardProps) {
return (
<Link href={`/posts/${id}`}>
<motion.div
layoutId={`card-container-${id}`}
className="rounded-xl overflow-hidden"
>
<motion.img
layoutId={`card-image-${id}`}
src={image}
alt={title}
className="w-full h-48 object-cover"
/>
<motion.div layoutId={`card-content-${id}`} className="p-4">
<motion.h3 layoutId={`card-title-${id}`} className="font-bold">
{title}
</motion.h3>
</motion.div>
</motion.div>
</Link>
);
}
// app/posts/[id]/page.tsx
"use client";
import { motion } from "framer-motion";
export default function PostPage({ params }: { params: { id: string } }) {
const { id } = params;
return (
<article>
<motion.div
layoutId={`card-container-${id}`}
className="max-w-3xl mx-auto"
>
<motion.img
layoutId={`card-image-${id}`}
src={`/images/${id}.jpg`}
alt="Post image"
className="w-full h-96 object-cover rounded-xl"
/>
<motion.div layoutId={`card-content-${id}`} className="p-6">
<motion.h1 layoutId={`card-title-${id}`} className="text-3xl font-bold">
Post Title
</motion.h1>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
>
Post content that fades in...
</motion.p>
</motion.div>
</motion.div>
</article>
);
}Full Page Slide Transition
const fullPageVariants = {
initial: (direction: number) => ({
x: direction > 0 ? "100%" : "-100%",
opacity: 0,
}),
enter: {
x: 0,
opacity: 1,
transition: {
duration: 0.4,
ease: [0.25, 0.1, 0.25, 1],
},
},
exit: (direction: number) => ({
x: direction > 0 ? "-100%" : "100%",
opacity: 0,
transition: {
duration: 0.4,
ease: [0.25, 0.1, 0.25, 1],
},
}),
};
export function FullPageTransition({ children, direction = 1 }) {
return (
<motion.div
custom={direction}
variants={fullPageVariants}
initial="initial"
animate="enter"
exit="exit"
className="fixed inset-0"
>
{children}
</motion.div>
);
}Overlay Page Transition
const overlayVariants = {
initial: {
y: "100%",
borderRadius: "100% 100% 0 0",
},
enter: {
y: 0,
borderRadius: "0% 0% 0 0",
transition: {
duration: 0.5,
ease: [0.76, 0, 0.24, 1],
},
},
exit: {
y: "100%",
borderRadius: "100% 100% 0 0",
transition: {
duration: 0.5,
ease: [0.76, 0, 0.24, 1],
},
},
};
export function OverlayTransition({ children }) {
return (
<motion.div
variants={overlayVariants}
initial="initial"
animate="enter"
exit="exit"
className="fixed inset-0 bg-background"
>
{children}
</motion.div>
);
}Page Transition with Loading
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { useState, useEffect } from "react";
import { usePathname } from "next/navigation";
export function PageWithLoader({ children }) {
const [isLoading, setIsLoading] = useState(true);
const pathname = usePathname();
useEffect(() => {
setIsLoading(true);
const timer = setTimeout(() => setIsLoading(false), 500);
return () => clearTimeout(timer);
}, [pathname]);
return (
<AnimatePresence mode="wait">
{isLoading ? (
<motion.div
key="loader"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 flex items-center justify-center"
>
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full"
/>
</motion.div>
) : (
<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>
);
}Best Practices
1. Keep transitions short: 300-500ms max for page transitions 2. Use `mode="wait"`: For cleaner transitions between pages 3. Match enter/exit: Exit should feel like reverse of enter 4. Avoid layout shifts: Use position: fixed during transitions 5. Stagger content: Animate child elements for richer feel 6. Test on mobile: Ensure smooth performance on lower-end devices 7. Respect reduced motion: Disable or simplify for prefers-reduced-motion
Scroll Animation Examples
Scroll-triggered animations and parallax effects.
Basic Scroll Reveal
"use client";
import { motion } from "framer-motion";
export function ScrollReveal({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5, ease: "easeOut" }}
>
{children}
</motion.div>
);
}
// Usage
<ScrollReveal>
<Card>Content appears when scrolled into view</Card>
</ScrollReveal>Staggered Scroll Reveal
"use client";
import { motion } from "framer-motion";
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5 },
},
};
export function StaggeredReveal({ items }: { items: any[] }) {
return (
<motion.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-50px" }}
className="grid grid-cols-3 gap-6"
>
{items.map((item) => (
<motion.div key={item.id} variants={itemVariants}>
{item.content}
</motion.div>
))}
</motion.div>
);
}Scroll Progress Indicator
"use client";
import { motion, useScroll, useSpring } from "framer-motion";
export function ScrollProgressBar() {
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001,
});
return (
<motion.div
style={{ scaleX }}
className="fixed top-0 left-0 right-0 h-1 bg-primary origin-left z-50"
/>
);
}Parallax Section
"use client";
import { useRef } from "react";
import { motion, useScroll, useTransform } from "framer-motion";
export 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.3, 0.7, 1], [0, 1, 1, 0]);
return (
<section ref={ref} className="h-screen relative overflow-hidden">
<motion.div
style={{ y, opacity }}
className="absolute inset-0 flex items-center justify-center"
>
<h2 className="text-6xl font-bold">Parallax Text</h2>
</motion.div>
</section>
);
}Parallax Background
"use client";
import { useRef } from "react";
import { motion, useScroll, useTransform } from "framer-motion";
export function ParallaxHero() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start start", "end start"],
});
const backgroundY = useTransform(scrollYProgress, [0, 1], ["0%", "50%"]);
const textY = useTransform(scrollYProgress, [0, 1], ["0%", "100%"]);
const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0]);
return (
<div ref={ref} className="relative h-screen overflow-hidden">
{/* Background image with parallax */}
<motion.div
style={{ y: backgroundY }}
className="absolute inset-0 bg-cover bg-center"
style={{
backgroundImage: "url(/hero-bg.jpg)",
y: backgroundY,
}}
/>
{/* Content */}
<motion.div
style={{ y: textY, opacity }}
className="relative z-10 flex h-full items-center justify-center"
>
<h1 className="text-6xl font-bold text-white">Hero Title</h1>
</motion.div>
</div>
);
}Scroll-Linked Animation
"use client";
import { useRef } from "react";
import { motion, useScroll, useTransform } from "framer-motion";
export function ScrollLinkedCard() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "center center"],
});
const scale = useTransform(scrollYProgress, [0, 1], [0.8, 1]);
const opacity = useTransform(scrollYProgress, [0, 1], [0.3, 1]);
const rotateX = useTransform(scrollYProgress, [0, 1], [20, 0]);
return (
<motion.div
ref={ref}
style={{ scale, opacity, rotateX, transformPerspective: 1000 }}
className="p-8 bg-card rounded-xl border"
>
Card that scales and rotates as you scroll
</motion.div>
);
}Horizontal Scroll Section
"use client";
import { useRef } from "react";
import { motion, useScroll, useTransform } from "framer-motion";
export function HorizontalScrollSection() {
const targetRef = useRef(null);
const { scrollYProgress } = useScroll({
target: targetRef,
});
const x = useTransform(scrollYProgress, [0, 1], ["0%", "-75%"]);
return (
<section ref={targetRef} className="relative h-[300vh]">
<div className="sticky top-0 h-screen flex items-center overflow-hidden">
<motion.div style={{ x }} className="flex gap-8">
{[1, 2, 3, 4].map((item) => (
<div
key={item}
className="w-[80vw] h-[60vh] shrink-0 bg-card rounded-xl border flex items-center justify-center"
>
<span className="text-4xl font-bold">Slide {item}</span>
</div>
))}
</motion.div>
</div>
</section>
);
}Reveal on Scroll with Different Directions
"use client";
import { motion } from "framer-motion";
type Direction = "up" | "down" | "left" | "right";
const directionVariants = {
up: { y: 50 },
down: { y: -50 },
left: { x: 50 },
right: { x: -50 },
};
export function DirectionalReveal({
children,
direction = "up",
}: {
children: React.ReactNode;
direction?: Direction;
}) {
return (
<motion.div
initial={{ opacity: 0, ...directionVariants[direction] }}
whileInView={{ opacity: 1, x: 0, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.6, ease: "easeOut" }}
>
{children}
</motion.div>
);
}
// Usage
<DirectionalReveal direction="left">
<Card>Slides in from the left</Card>
</DirectionalReveal>Number Counter on Scroll
"use client";
import { useRef, useEffect, useState } from "react";
import { motion, useInView, animate } from "framer-motion";
export function CountUp({
target,
duration = 2,
}: {
target: number;
duration?: number;
}) {
const ref = useRef(null);
const isInView = useInView(ref, { once: true });
const [count, setCount] = useState(0);
useEffect(() => {
if (isInView) {
const controls = animate(0, target, {
duration,
onUpdate: (value) => setCount(Math.floor(value)),
});
return () => controls.stop();
}
}, [isInView, target, duration]);
return (
<motion.span
ref={ref}
initial={{ opacity: 0 }}
animate={isInView ? { opacity: 1 } : {}}
className="text-5xl font-bold"
>
{count.toLocaleString()}
</motion.span>
);
}Scroll Snap with Animations
"use client";
import { useRef } from "react";
import { motion, useScroll, useTransform } from "framer-motion";
const sections = [
{ id: 1, title: "Section One", color: "bg-blue-500" },
{ id: 2, title: "Section Two", color: "bg-green-500" },
{ id: 3, title: "Section Three", color: "bg-purple-500" },
];
export function ScrollSnapSections() {
return (
<div className="h-screen overflow-y-scroll snap-y snap-mandatory">
{sections.map((section) => (
<ScrollSnapSection key={section.id} {...section} />
))}
</div>
);
}
function ScrollSnapSection({
title,
color,
}: {
title: string;
color: string;
}) {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"],
});
const scale = useTransform(scrollYProgress, [0, 0.5, 1], [0.8, 1, 0.8]);
const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0.3, 1, 0.3]);
return (
<section
ref={ref}
className={`h-screen snap-start flex items-center justify-center ${color}`}
>
<motion.h2 style={{ scale, opacity }} className="text-6xl font-bold text-white">
{title}
</motion.h2>
</section>
);
}Scroll-Triggered Path Animation
"use client";
import { useRef } from "react";
import { motion, useScroll, useTransform } from "framer-motion";
export function ScrollPathAnimation() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"],
});
const pathLength = useTransform(scrollYProgress, [0, 0.5], [0, 1]);
return (
<div ref={ref} className="h-[200vh] flex items-center justify-center">
<svg width="200" height="200" viewBox="0 0 100 100" className="stroke-primary">
<motion.circle
cx="50"
cy="50"
r="40"
fill="none"
strokeWidth="4"
style={{ pathLength }}
/>
</svg>
</div>
);
}Best Practices
1. Use `viewport={{ once: true }}`: Prevents re-triggering on scroll back 2. Add margin to viewport: Trigger slightly before element is visible 3. Use `useSpring` for progress: Smoother progress bar animations 4. Keep parallax subtle: Small movements (50-100px) feel more natural 5. Test performance: Heavy scroll animations can impact mobile performance 6. Consider reduced motion: Disable parallax for prefers-reduced-motion
Gestures Reference
Framer Motion provides gesture recognition for hover, tap, pan, and drag.
Hover Gestures
Basic Hover
<motion.div
whileHover={{ scale: 1.1 }}
onHoverStart={() => console.log("Hover started")}
onHoverEnd={() => console.log("Hover ended")}
>
Hover me
</motion.div>Hover with Transition
<motion.button
whileHover={{
scale: 1.05,
backgroundColor: "#3b82f6",
color: "#ffffff",
}}
transition={{ duration: 0.2 }}
>
Hover Button
</motion.button>Hover Card Effect
<motion.div
whileHover={{
y: -5,
boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.1)",
}}
transition={{ type: "spring", stiffness: 300 }}
className="p-6 rounded-lg border"
>
Card content
</motion.div>Tap Gestures
Basic Tap
<motion.button
whileTap={{ scale: 0.95 }}
onTap={() => console.log("Tapped!")}
>
Click me
</motion.button>Tap Events
<motion.button
whileTap={{ scale: 0.95 }}
onTapStart={(event, info) => {
console.log("Tap started at", info.point);
}}
onTap={(event, info) => {
console.log("Tap completed at", info.point);
}}
onTapCancel={() => {
console.log("Tap cancelled");
}}
>
Button
</motion.button>Combined Hover + Tap
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
Interactive Button
</motion.button>Focus Gestures
<motion.input
whileFocus={{
scale: 1.02,
borderColor: "#3b82f6",
boxShadow: "0 0 0 2px rgba(59, 130, 246, 0.5)",
}}
className="px-4 py-2 border rounded"
/>Pan Gestures
Pan recognizes movement without dragging.
<motion.div
onPan={(event, info) => {
console.log("Delta:", info.delta.x, info.delta.y);
console.log("Offset:", info.offset.x, info.offset.y);
console.log("Point:", info.point.x, info.point.y);
console.log("Velocity:", info.velocity.x, info.velocity.y);
}}
onPanStart={(event, info) => console.log("Pan started")}
onPanEnd={(event, info) => console.log("Pan ended")}
>
Pan me
</motion.div>Swipe Detection
function SwipeCard({ onSwipe }) {
return (
<motion.div
onPanEnd={(event, info) => {
const threshold = 100;
const velocity = 500;
if (info.offset.x > threshold || info.velocity.x > velocity) {
onSwipe("right");
} else if (info.offset.x < -threshold || info.velocity.x < -velocity) {
onSwipe("left");
}
}}
>
Swipe me
</motion.div>
);
}Drag Gestures
Basic Drag
<motion.div drag>
Drag me anywhere
</motion.div>
// Constrained to axis
<motion.div drag="x">Horizontal only</motion.div>
<motion.div drag="y">Vertical only</motion.div>Drag Constraints
// Pixel constraints
<motion.div
drag
dragConstraints={{
top: -100,
left: -100,
right: 100,
bottom: 100,
}}
>
Constrained drag
</motion.div>
// Reference element
const constraintsRef = useRef(null);
<div ref={constraintsRef} className="w-96 h-96 border">
<motion.div
drag
dragConstraints={constraintsRef}
className="w-20 h-20 bg-blue-500 rounded"
/>
</div>Drag Elasticity
<motion.div
drag
dragConstraints={{ left: 0, right: 0, top: 0, bottom: 0 }}
dragElastic={0.2} // 0 = no elasticity, 1 = full elasticity
>
Elastic drag
</motion.div>Drag Momentum
<motion.div
drag
dragMomentum={true} // Enable momentum (default)
dragTransition={{ bounceStiffness: 600, bounceDamping: 20 }}
>
Momentum drag
</motion.div>Drag Snap to Origin
<motion.div drag dragSnapToOrigin>
Snaps back when released
</motion.div>Drag Events
<motion.div
drag
onDragStart={(event, info) => {
console.log("Drag started at", info.point);
}}
onDrag={(event, info) => {
console.log("Dragging:", info.point, info.delta, info.offset, info.velocity);
}}
onDragEnd={(event, info) => {
console.log("Drag ended at", info.point);
console.log("Velocity:", info.velocity);
}}
>
Drag me
</motion.div>Drag Direction Lock
<motion.div
drag
dragDirectionLock
onDirectionLock={(axis) => console.log(`Locked to ${axis}`)}
>
Locks to first detected direction
</motion.div>Drag Controls
import { motion, useDragControls } from "framer-motion";
function DraggableCard() {
const dragControls = useDragControls();
return (
<>
{/* Handle to initiate drag */}
<div
onPointerDown={(e) => dragControls.start(e)}
className="cursor-grab"
>
Drag handle
</div>
<motion.div drag dragControls={dragControls} dragListener={false}>
Draggable content (only via handle)
</motion.div>
</>
);
}While Dragging Animation
<motion.div
drag
whileDrag={{
scale: 1.1,
cursor: "grabbing",
boxShadow: "0 10px 30px rgba(0,0,0,0.2)",
}}
className="cursor-grab"
>
Drag me
</motion.div>Sortable List (Reorder)
import { Reorder } from "framer-motion";
function SortableList() {
const [items, setItems] = useState([1, 2, 3, 4]);
return (
<Reorder.Group
axis="y"
values={items}
onReorder={setItems}
className="space-y-2"
>
{items.map((item) => (
<Reorder.Item
key={item}
value={item}
className="p-4 bg-white rounded shadow cursor-grab"
>
Item {item}
</Reorder.Item>
))}
</Reorder.Group>
);
}Custom Drag Handle for Reorder
import { Reorder, useDragControls } from "framer-motion";
function SortableItem({ item }) {
const dragControls = useDragControls();
return (
<Reorder.Item
value={item}
dragControls={dragControls}
dragListener={false}
className="flex items-center gap-2 p-4 bg-white rounded"
>
<div
onPointerDown={(e) => dragControls.start(e)}
className="cursor-grab p-1"
>
<GripVertical className="h-4 w-4" />
</div>
<span>{item.name}</span>
</Reorder.Item>
);
}Gesture Propagation
Control which element responds to gestures:
// Stop propagation
<motion.div whileTap={{ scale: 0.95 }}>
<motion.button
whileTap={{ scale: 1.1 }}
// Child tap doesn't trigger parent
>
Button
</motion.button>
</motion.div>Best Practices
1. Use springs for interactions: More natural feel than tween 2. Keep scale changes subtle: 0.95-1.05 range for tap/hover 3. Add visual feedback: Shadow, color changes for hover 4. Use drag constraints: Prevent elements from being lost off-screen 5. Handle touch devices: Hover animations may not work on touch 6. Respect reduced motion: Skip animations for users who prefer reduced motion
Animation Hooks Reference
Framer Motion provides hooks for advanced animation control.
useAnimation
Programmatic control over animations.
import { motion, useAnimation } from "framer-motion";
function Component() {
const controls = useAnimation();
async function sequence() {
await controls.start({ x: 100 });
await controls.start({ y: 100 });
await controls.start({ x: 0, y: 0 });
}
return (
<>
<button onClick={sequence}>Start sequence</button>
<motion.div animate={controls}>
Controlled animation
</motion.div>
</>
);
}Control Methods
const controls = useAnimation();
// Start animation
controls.start({ opacity: 1, x: 100 });
// Start with variant
controls.start("visible");
// Start with transition
controls.start({ x: 100 }, { duration: 0.5 });
// Stop animation
controls.stop();
// Set values immediately (no animation)
controls.set({ x: 0, opacity: 0 });Orchestrating Multiple Elements
function Component() {
const boxControls = useAnimation();
const circleControls = useAnimation();
async function playSequence() {
await boxControls.start({ x: 100 });
await circleControls.start({ scale: 1.5 });
await Promise.all([
boxControls.start({ x: 0 }),
circleControls.start({ scale: 1 }),
]);
}
return (
<>
<motion.div animate={boxControls}>Box</motion.div>
<motion.div animate={circleControls}>Circle</motion.div>
<button onClick={playSequence}>Play</button>
</>
);
}useMotionValue
Create reactive values for animations.
import { motion, useMotionValue } from "framer-motion";
function Component() {
const x = useMotionValue(0);
return (
<motion.div
style={{ x }}
drag="x"
onDrag={(event, info) => {
console.log(x.get()); // Get current value
}}
>
Drag me
</motion.div>
);
}MotionValue Methods
const x = useMotionValue(0);
// Get current value
const current = x.get();
// Set value (no animation)
x.set(100);
// Subscribe to changes
const unsubscribe = x.on("change", (latest) => {
console.log("x changed to", latest);
});
// Jump to value (skips animation)
x.jump(100);
// Check if animating
const isAnimating = x.isAnimating();
// Get velocity
const velocity = x.getVelocity();useTransform
Transform one motion value into another.
import { motion, useMotionValue, useTransform } from "framer-motion";
function Component() {
const x = useMotionValue(0);
// Transform x (0-200) to opacity (1-0)
const opacity = useTransform(x, [0, 200], [1, 0]);
// Transform x to rotation
const rotate = useTransform(x, [0, 200], [0, 180]);
// Transform x to scale
const scale = useTransform(x, [-100, 0, 100], [0.5, 1, 1.5]);
return (
<motion.div
drag="x"
style={{ x, opacity, rotate, scale }}
>
Drag me
</motion.div>
);
}Chained Transforms
const x = useMotionValue(0);
const xRange = useTransform(x, [0, 100], [0, 1]);
const opacity = useTransform(xRange, [0, 0.5, 1], [0, 1, 0]);Custom Transform Function
const x = useMotionValue(0);
const background = useTransform(x, (value) => {
return value > 0 ? "#22c55e" : "#ef4444";
});useSpring
Create spring-animated motion values.
import { motion, useSpring, useMotionValue } from "framer-motion";
function Component() {
const x = useMotionValue(0);
const springX = useSpring(x, { stiffness: 300, damping: 30 });
return (
<motion.div
style={{ x: springX }}
onMouseMove={(e) => x.set(e.clientX)}
>
Follows cursor with spring
</motion.div>
);
}Spring Options
const springValue = useSpring(motionValue, {
stiffness: 300, // Higher = snappier
damping: 30, // Higher = less bounce
mass: 1, // Higher = more momentum
velocity: 0, // Initial velocity
restSpeed: 0.01, // Minimum speed to consider "at rest"
restDelta: 0.01, // Minimum distance to consider "at rest"
});useScroll
Track scroll progress.
import { motion, useScroll, useTransform } from "framer-motion";
function ScrollProgress() {
const { scrollYProgress } = useScroll();
return (
<motion.div
style={{ scaleX: scrollYProgress }}
className="fixed top-0 left-0 right-0 h-1 bg-primary origin-left"
/>
);
}Scroll Container
function Component() {
const containerRef = useRef(null);
const { scrollYProgress } = useScroll({
container: containerRef,
});
return (
<div ref={containerRef} className="h-[400px] overflow-y-scroll">
<motion.div style={{ opacity: scrollYProgress }}>
Fades in as you scroll
</motion.div>
</div>
);
}Scroll Target Element
function Component() {
const targetRef = useRef(null);
const { scrollYProgress } = useScroll({
target: targetRef,
offset: ["start end", "end start"], // When to start/end tracking
});
return (
<motion.div
ref={targetRef}
style={{ opacity: scrollYProgress }}
>
Animates as it passes through viewport
</motion.div>
);
}Scroll Offset Options
const { scrollYProgress } = useScroll({
target: ref,
offset: [
"start end", // When target's start reaches viewport's end
"end start", // When target's end reaches viewport's start
],
});
// Other offset values:
// "start", "center", "end" - element positions
// Numbers: pixels (100) or percentages (0.5)useVelocity
Get velocity of a motion value.
import { useMotionValue, useVelocity } from "framer-motion";
function Component() {
const x = useMotionValue(0);
const xVelocity = useVelocity(x);
return (
<motion.div
drag="x"
style={{ x }}
onDragEnd={() => {
console.log("Release velocity:", xVelocity.get());
}}
>
Drag me
</motion.div>
);
}useInView
Detect when element enters viewport.
import { useInView } from "framer-motion";
function Component() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true });
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 50 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5 }}
>
Animates when scrolled into view
</motion.div>
);
}InView Options
const isInView = useInView(ref, {
once: true, // Only trigger once
amount: 0.5, // Trigger when 50% visible
margin: "-100px", // Adjust trigger point
root: scrollContainerRef, // Custom scroll container
});useReducedMotion
Detect reduced motion preference.
import { useReducedMotion } from "framer-motion";
function Component() {
const prefersReducedMotion = useReducedMotion();
return (
<motion.div
animate={{ x: 100 }}
transition={{
duration: prefersReducedMotion ? 0 : 0.5,
}}
>
Respects motion preference
</motion.div>
);
}useDragControls
Create custom drag handles.
import { motion, useDragControls } from "framer-motion";
function DraggableCard() {
const dragControls = useDragControls();
return (
<motion.div
drag
dragControls={dragControls}
dragListener={false} // Disable drag on whole element
>
<div
onPointerDown={(e) => dragControls.start(e)}
className="cursor-grab"
>
Drag Handle
</div>
<div>Card Content (not draggable)</div>
</motion.div>
);
}useAnimationFrame
Run code every animation frame.
import { useAnimationFrame } from "framer-motion";
function Component() {
const ref = useRef(null);
useAnimationFrame((time, delta) => {
// time: total time elapsed (ms)
// delta: time since last frame (ms)
if (ref.current) {
ref.current.style.transform = `rotate(${time / 10}deg)`;
}
});
return <div ref={ref}>Spinning</div>;
}Combining Hooks
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 (
<motion.section
ref={ref}
style={{ y, opacity }}
className="h-screen"
>
Parallax content
</motion.section>
);
}Motion Component Reference
The motion component is the core building block of Framer Motion.
Basic Usage
import { motion } from "framer-motion";
// Any HTML element can be animated
<motion.div />
<motion.span />
<motion.button />
<motion.ul />
<motion.li />
<motion.svg />
<motion.path />
<motion.img />Animation Props
initial
The initial state before animation begins.
<motion.div initial={{ opacity: 0, scale: 0.5 }}>
Starts invisible and small
</motion.div>
// Can be false to disable initial animation
<motion.div initial={false} animate={{ x: 100 }}>
Animates immediately without initial state
</motion.div>
// Can reference a variant
<motion.div initial="hidden" animate="visible" variants={variants}>animate
The target state to animate to.
<motion.div animate={{ opacity: 1, x: 100 }}>
Animates to these values
</motion.div>
// Can be a variant name
<motion.div animate="visible" variants={variants}>
// Can be controlled by state
<motion.div animate={{ x: isActive ? 100 : 0 }}>exit
The state to animate to when removed (requires AnimatePresence).
import { AnimatePresence, motion } from "framer-motion";
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
I animate out when removed
</motion.div>
)}
</AnimatePresence>transition
Controls how the animation behaves.
<motion.div
animate={{ x: 100 }}
transition={{
duration: 0.5, // Seconds
delay: 0.2, // Seconds
ease: "easeInOut", // Easing function
repeat: Infinity, // Number of repeats
repeatType: "reverse", // "loop" | "reverse" | "mirror"
repeatDelay: 0.5, // Delay between repeats
}}
>
// Spring animation
<motion.div
animate={{ x: 100 }}
transition={{
type: "spring",
stiffness: 300, // Higher = snappier
damping: 20, // Higher = less bounce
mass: 1, // Higher = more momentum
}}
>
// Spring with bounce
<motion.div
animate={{ x: 100 }}
transition={{
type: "spring",
bounce: 0.25, // 0 = no bounce, 1 = max bounce
duration: 0.6, // Target duration
}}
>Gesture Props
whileHover
Animate while hovering.
<motion.button
whileHover={{ scale: 1.1, backgroundColor: "#f00" }}
>
Hover me
</motion.button>
// With transition
<motion.button
whileHover={{ scale: 1.1 }}
transition={{ type: "spring", stiffness: 400 }}
>whileTap
Animate while pressing/clicking.
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
Click me
</motion.button>whileFocus
Animate while focused.
<motion.input
whileFocus={{ scale: 1.02, borderColor: "#3b82f6" }}
/>whileInView
Animate when element enters viewport.
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
>
Animates when scrolled into view
</motion.div>whileDrag
Animate while dragging.
<motion.div
drag
whileDrag={{ scale: 1.1, cursor: "grabbing" }}
>
Drag me
</motion.div>Drag Props
drag
Enable dragging.
// Drag in any direction
<motion.div drag>Drag me</motion.div>
// Drag only on x-axis
<motion.div drag="x">Horizontal only</motion.div>
// Drag only on y-axis
<motion.div drag="y">Vertical only</motion.div>dragConstraints
Limit drag area.
// Pixel constraints
<motion.div
drag
dragConstraints={{ top: -50, left: -50, right: 50, bottom: 50 }}
>
// Reference another element
const constraintsRef = useRef(null);
<div ref={constraintsRef} className="w-[500px] h-[500px]">
<motion.div
drag
dragConstraints={constraintsRef}
>
Constrained within parent
</motion.div>
</div>dragElastic
How far element can be dragged past constraints (0-1).
<motion.div
drag
dragConstraints={{ left: 0, right: 0 }}
dragElastic={0.1}
>
Slightly elastic
</motion.div>dragSnapToOrigin
Return to original position when released.
<motion.div drag dragSnapToOrigin>
Snaps back when released
</motion.div>Layout Props
layout
Enable layout animations.
// Animate when layout changes
<motion.div layout>
Content that may change size
</motion.div>
// Only animate position
<motion.div layout="position">
// Only animate size
<motion.div layout="size">layoutId
Enable shared element transitions.
// In list view
<motion.div layoutId={`card-${id}`}>
Card thumbnail
</motion.div>
// In detail view (same layoutId = smooth transition)
<motion.div layoutId={`card-${id}`}>
Card expanded
</motion.div>Style Props
Transform properties are GPU-accelerated:
<motion.div
animate={{
// Transform (GPU accelerated)
x: 100, // translateX
y: 100, // translateY
z: 100, // translateZ
rotate: 45, // rotate (degrees)
rotateX: 45, // rotate3d X
rotateY: 45, // rotate3d Y
rotateZ: 45, // rotate3d Z
scale: 1.5, // scale
scaleX: 1.5, // scaleX
scaleY: 1.5, // scaleY
skew: 10, // skew (degrees)
skewX: 10, // skewX
skewY: 10, // skewY
// Opacity
opacity: 0.5,
// Colors
backgroundColor: "#ff0000",
color: "#ffffff",
borderColor: "#000000",
// Size (not GPU accelerated)
width: 100,
height: 100,
// Other CSS properties
borderRadius: 10,
boxShadow: "0 10px 20px rgba(0,0,0,0.2)",
}}
>Event Callbacks
<motion.div
// Animation events
onAnimationStart={() => console.log("Animation started")}
onAnimationComplete={() => console.log("Animation complete")}
// Hover events
onHoverStart={() => console.log("Hover start")}
onHoverEnd={() => console.log("Hover end")}
// Tap events
onTap={() => console.log("Tapped")}
onTapStart={() => console.log("Tap start")}
onTapCancel={() => console.log("Tap cancelled")}
// Drag events
onDrag={(event, info) => console.log(info.point.x, info.point.y)}
onDragStart={(event, info) => console.log("Drag started")}
onDragEnd={(event, info) => console.log("Drag ended")}
// Pan events
onPan={(event, info) => console.log(info.delta.x)}
onPanStart={(event, info) => console.log("Pan started")}
onPanEnd={(event, info) => console.log("Pan ended")}
// Viewport events
onViewportEnter={() => console.log("Entered viewport")}
onViewportLeave={() => console.log("Left viewport")}
>Viewport Options
<motion.div
whileInView={{ opacity: 1 }}
viewport={{
once: true, // Only animate once
amount: 0.5, // Trigger when 50% visible (0-1)
margin: "-100px", // Adjust trigger point
root: scrollRef, // Custom scroll container
}}
>Custom Components
import { motion } from "framer-motion";
import { Button } from "@/components/ui/button";
// Create motion version of custom component
const MotionButton = motion(Button);
<MotionButton
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
Animated Button
</MotionButton>SVG Animation
<motion.svg viewBox="0 0 100 100">
<motion.circle
cx="50"
cy="50"
r="40"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 2, ease: "easeInOut" }}
/>
<motion.path
d="M10 10 L90 90"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 1 }}
/>
</motion.svg>Variants Reference
Variants are predefined animation states that simplify complex animations.
Basic Variants
const variants = {
hidden: { opacity: 0 },
visible: { opacity: 1 },
};
<motion.div
variants={variants}
initial="hidden"
animate="visible"
>
Fades in
</motion.div>Multiple Properties
const variants = {
hidden: {
opacity: 0,
y: 20,
scale: 0.95,
},
visible: {
opacity: 1,
y: 0,
scale: 1,
},
};
<motion.div variants={variants} initial="hidden" animate="visible">
Fades in, slides up, and scales
</motion.div>Transitions in Variants
const variants = {
hidden: {
opacity: 0,
y: 20,
},
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
ease: "easeOut",
},
},
exit: {
opacity: 0,
y: -20,
transition: {
duration: 0.3,
},
},
};Parent-Child Orchestration
Children automatically inherit variants from parents:
const container = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
when: "beforeChildren", // Animate parent first
staggerChildren: 0.1, // Delay between children
delayChildren: 0.3, // Delay before first child
},
},
};
const item = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
};
<motion.ul variants={container} initial="hidden" animate="visible">
<motion.li variants={item}>Item 1</motion.li>
<motion.li variants={item}>Item 2</motion.li>
<motion.li variants={item}>Item 3</motion.li>
</motion.ul>Stagger Options
const container = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
staggerDirection: 1, // 1 = forward, -1 = reverse
delayChildren: 0.2,
},
},
exit: {
opacity: 0,
transition: {
staggerChildren: 0.05,
staggerDirection: -1, // Reverse stagger on exit
when: "afterChildren", // Wait for children to exit
},
},
};When Property
const variants = {
visible: {
opacity: 1,
transition: {
when: "beforeChildren", // Parent animates first
// or
when: "afterChildren", // Children animate first
},
},
};Dynamic Variants
Pass custom values to variants:
const variants = {
hidden: { opacity: 0 },
visible: (custom: number) => ({
opacity: 1,
transition: { delay: custom * 0.1 },
}),
};
<motion.ul initial="hidden" animate="visible">
{items.map((item, i) => (
<motion.li
key={item.id}
variants={variants}
custom={i} // Pass index to variant
>
{item.name}
</motion.li>
))}
</motion.ul>Hover/Tap Variants
const buttonVariants = {
initial: {
scale: 1,
backgroundColor: "#3b82f6",
},
hover: {
scale: 1.05,
backgroundColor: "#2563eb",
},
tap: {
scale: 0.95,
},
};
<motion.button
variants={buttonVariants}
initial="initial"
whileHover="hover"
whileTap="tap"
>
Click me
</motion.button>Complex Card Example
const cardVariants = {
hidden: {
opacity: 0,
y: 20,
scale: 0.95,
},
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: {
duration: 0.4,
ease: "easeOut",
when: "beforeChildren",
staggerChildren: 0.1,
},
},
hover: {
y: -5,
boxShadow: "0 10px 30px -10px rgba(0,0,0,0.2)",
transition: {
duration: 0.2,
},
},
};
const contentVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1 },
};
<motion.div
variants={cardVariants}
initial="hidden"
animate="visible"
whileHover="hover"
>
<motion.h3 variants={contentVariants}>Title</motion.h3>
<motion.p variants={contentVariants}>Description</motion.p>
<motion.button variants={contentVariants}>Action</motion.button>
</motion.div>List Animation
const listVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.07,
delayChildren: 0.2,
},
},
exit: {
opacity: 0,
transition: {
staggerChildren: 0.05,
staggerDirection: -1,
},
},
};
const itemVariants = {
hidden: {
y: 20,
opacity: 0,
},
visible: {
y: 0,
opacity: 1,
transition: {
type: "spring",
stiffness: 300,
damping: 24,
},
},
exit: {
y: -20,
opacity: 0,
},
};
<AnimatePresence mode="popLayout">
<motion.ul
variants={listVariants}
initial="hidden"
animate="visible"
exit="exit"
>
{items.map((item) => (
<motion.li
key={item.id}
variants={itemVariants}
layout
>
{item.name}
</motion.li>
))}
</motion.ul>
</AnimatePresence>Page Transition Variants
const pageVariants = {
initial: {
opacity: 0,
x: -20,
},
enter: {
opacity: 1,
x: 0,
transition: {
duration: 0.4,
ease: "easeOut",
},
},
exit: {
opacity: 0,
x: 20,
transition: {
duration: 0.3,
ease: "easeIn",
},
},
};
// In your page component
<motion.div
variants={pageVariants}
initial="initial"
animate="enter"
exit="exit"
>
Page content
</motion.div>Sidebar Variants
const sidebarVariants = {
open: {
x: 0,
transition: {
type: "spring",
stiffness: 300,
damping: 30,
when: "beforeChildren",
staggerChildren: 0.05,
},
},
closed: {
x: "-100%",
transition: {
type: "spring",
stiffness: 400,
damping: 40,
when: "afterChildren",
staggerChildren: 0.05,
staggerDirection: -1,
},
},
};
const linkVariants = {
open: {
opacity: 1,
x: 0,
},
closed: {
opacity: 0,
x: -20,
},
};
<motion.aside
variants={sidebarVariants}
initial="closed"
animate={isOpen ? "open" : "closed"}
>
<nav>
{links.map((link) => (
<motion.a key={link.href} href={link.href} variants={linkVariants}>
{link.label}
</motion.a>
))}
</nav>
</motion.aside>Best Practices
1. Use semantic variant names: hidden/visible, open/closed, enter/exit 2. Define transitions in variants: Keeps animation logic together 3. Orchestrate with parent: Use staggerChildren, delayChildren, when 4. Children inherit variant names: No need to set initial/animate on children 5. Use `custom` for dynamic values: Index-based delays, direction, etc.
/**
* Animated List Template
*
* A comprehensive animated list component with:
* - Staggered entrance animations
* - Smooth entry/exit for items
* - Drag-to-reorder functionality
* - Item removal animations
*
* Usage:
* ```tsx
* import { AnimatedList, AnimatedListItem } from "@/components/animated-list";
*
* function MyList() {
* const [items, setItems] = useState([...]);
*
* return (
* <AnimatedList>
* {items.map((item) => (
* <AnimatedListItem key={item.id}>
* {item.content}
* </AnimatedListItem>
* ))}
* </AnimatedList>
* );
* }
* ```
*/
"use client";
import { ReactNode, useState } from "react";
import {
AnimatePresence,
motion,
Reorder,
useDragControls,
Variants,
} from "framer-motion";
import { GripVertical, X } from "lucide-react";
// ============================================================================
// Animation Variants
// ============================================================================
const containerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.08,
delayChildren: 0.1,
},
},
};
const itemVariants: Variants = {
hidden: {
opacity: 0,
y: 20,
scale: 0.95,
},
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: {
type: "spring",
stiffness: 300,
damping: 24,
},
},
exit: {
opacity: 0,
scale: 0.9,
x: -20,
transition: {
duration: 0.2,
},
},
};
// ============================================================================
// Basic Animated List (No Reordering)
// ============================================================================
interface AnimatedListProps {
children: ReactNode;
className?: string;
}
/**
* AnimatedList - Container with staggered children animation
*
* Use with AnimatedListItem for individual item animations.
*/
export function AnimatedList({ children, className }: AnimatedListProps) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
className={className}
>
{children}
</motion.ul>
);
}
interface AnimatedListItemProps {
children: ReactNode;
className?: string;
/**
* Called when remove button is clicked
*/
onRemove?: () => void;
/**
* Show remove button on hover
* @default false
*/
showRemove?: boolean;
}
/**
* AnimatedListItem - Individual list item with animations
*
* Features:
* - Enters with staggered spring animation
* - Exit animation when removed
* - Optional remove button on hover
*/
export function AnimatedListItem({
children,
className,
onRemove,
showRemove = false,
}: AnimatedListItemProps) {
return (
<motion.li
layout
variants={itemVariants}
exit="exit"
className={`group relative ${className || ""}`}
>
{children}
{showRemove && onRemove && (
<motion.button
initial={{ opacity: 0, scale: 0.8 }}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
className="absolute right-2 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity p-1 text-muted-foreground hover:text-destructive"
onClick={onRemove}
>
<X className="h-4 w-4" />
</motion.button>
)}
</motion.li>
);
}
// ============================================================================
// Animated List with Entry/Exit (AnimatePresence)
// ============================================================================
interface DynamicListProps<T> {
items: T[];
keyExtractor: (item: T) => string;
renderItem: (item: T, index: number) => ReactNode;
className?: string;
}
/**
* DynamicList - List with smooth add/remove animations
*
* Wraps items in AnimatePresence for exit animations.
*
* @example
* ```tsx
* <DynamicList
* items={todos}
* keyExtractor={(todo) => todo.id}
* renderItem={(todo) => <TodoItem todo={todo} />}
* />
* ```
*/
export function DynamicList<T>({
items,
keyExtractor,
renderItem,
className,
}: DynamicListProps<T>) {
return (
<motion.ul layout className={className}>
<AnimatePresence mode="popLayout">
{items.map((item, index) => (
<motion.li
key={keyExtractor(item)}
layout
initial={{ opacity: 0, scale: 0.8, y: -10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.8, x: -50 }}
transition={{
type: "spring",
stiffness: 300,
damping: 24,
}}
>
{renderItem(item, index)}
</motion.li>
))}
</AnimatePresence>
</motion.ul>
);
}
// ============================================================================
// Reorderable List (Drag to Reorder)
// ============================================================================
interface ReorderableListProps<T> {
items: T[];
onReorder: (items: T[]) => void;
keyExtractor: (item: T) => string;
renderItem: (item: T, dragControls: ReturnType<typeof useDragControls>) => ReactNode;
className?: string;
/**
* Axis for reordering
* @default "y"
*/
axis?: "x" | "y";
}
/**
* ReorderableList - Drag-to-reorder list
*
* Uses Framer Motion's Reorder component for smooth reordering.
*
* @example
* ```tsx
* const [items, setItems] = useState(initialItems);
*
* <ReorderableList
* items={items}
* onReorder={setItems}
* keyExtractor={(item) => item.id}
* renderItem={(item, dragControls) => (
* <ReorderableItem item={item} dragControls={dragControls} />
* )}
* />
* ```
*/
export function ReorderableList<T>({
items,
onReorder,
keyExtractor,
renderItem,
className,
axis = "y",
}: ReorderableListProps<T>) {
return (
<Reorder.Group
axis={axis}
values={items}
onReorder={onReorder}
className={className}
>
{items.map((item) => (
<ReorderableItemWrapper
key={keyExtractor(item)}
item={item}
renderItem={renderItem}
/>
))}
</Reorder.Group>
);
}
// Internal wrapper to provide drag controls
function ReorderableItemWrapper<T>({
item,
renderItem,
}: {
item: T;
renderItem: (item: T, dragControls: ReturnType<typeof useDragControls>) => ReactNode;
}) {
const dragControls = useDragControls();
return (
<Reorder.Item
value={item}
dragControls={dragControls}
dragListener={false}
>
{renderItem(item, dragControls)}
</Reorder.Item>
);
}
// ============================================================================
// Drag Handle Component
// ============================================================================
interface DragHandleProps {
dragControls: ReturnType<typeof useDragControls>;
className?: string;
}
/**
* DragHandle - Grab handle for reorderable items
*
* @example
* ```tsx
* renderItem={(item, dragControls) => (
* <div className="flex items-center gap-2">
* <DragHandle dragControls={dragControls} />
* <span>{item.name}</span>
* </div>
* )}
* ```
*/
export function DragHandle({ dragControls, className }: DragHandleProps) {
return (
<div
onPointerDown={(e) => dragControls.start(e)}
className={`cursor-grab active:cursor-grabbing touch-none ${className || ""}`}
>
<GripVertical className="h-5 w-5 text-muted-foreground" />
</div>
);
}
// ============================================================================
// Complete Reorderable Todo List Example
// ============================================================================
interface TodoItem {
id: string;
text: string;
completed: boolean;
}
interface ReorderableTodoListProps {
initialItems?: TodoItem[];
}
/**
* ReorderableTodoList - Complete example of an animated, reorderable todo list
*
* Features:
* - Drag to reorder
* - Add new items
* - Remove items with animation
* - Toggle completion state
*/
export function ReorderableTodoList({
initialItems = [],
}: ReorderableTodoListProps) {
const [items, setItems] = useState<TodoItem[]>(initialItems);
const [newItemText, setNewItemText] = useState("");
function addItem() {
if (!newItemText.trim()) return;
setItems([
...items,
{
id: crypto.randomUUID(),
text: newItemText.trim(),
completed: false,
},
]);
setNewItemText("");
}
function removeItem(id: string) {
setItems(items.filter((item) => item.id !== id));
}
function toggleItem(id: string) {
setItems(
items.map((item) =>
item.id === id ? { ...item, completed: !item.completed } : item
)
);
}
return (
<div className="space-y-4">
{/* Add item form */}
<div className="flex gap-2">
<input
type="text"
value={newItemText}
onChange={(e) => setNewItemText(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && addItem()}
placeholder="Add new item..."
className="flex-1 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary outline-none"
/>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={addItem}
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg"
>
Add
</motion.button>
</div>
{/* Reorderable list */}
<Reorder.Group
axis="y"
values={items}
onReorder={setItems}
className="space-y-2"
>
<AnimatePresence mode="popLayout">
{items.map((item) => (
<TodoListItem
key={item.id}
item={item}
onToggle={() => toggleItem(item.id)}
onRemove={() => removeItem(item.id)}
/>
))}
</AnimatePresence>
</Reorder.Group>
{/* Empty state */}
{items.length === 0 && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center text-muted-foreground py-8"
>
No items yet. Add one above!
</motion.p>
)}
</div>
);
}
// Internal todo item component
function TodoListItem({
item,
onToggle,
onRemove,
}: {
item: TodoItem;
onToggle: () => void;
onRemove: () => void;
}) {
const dragControls = useDragControls();
return (
<Reorder.Item
value={item}
dragControls={dragControls}
dragListener={false}
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, x: -50 }}
transition={{ type: "spring", stiffness: 300, damping: 24 }}
className="flex items-center gap-3 p-4 bg-card rounded-lg border"
>
{/* Drag handle */}
<div
onPointerDown={(e) => dragControls.start(e)}
className="cursor-grab active:cursor-grabbing touch-none"
>
<GripVertical className="h-5 w-5 text-muted-foreground" />
</div>
{/* Checkbox */}
<motion.input
type="checkbox"
checked={item.completed}
onChange={onToggle}
whileTap={{ scale: 0.9 }}
className="h-4 w-4"
/>
{/* Text */}
<motion.span
animate={{
opacity: item.completed ? 0.5 : 1,
textDecoration: item.completed ? "line-through" : "none",
}}
className="flex-1"
>
{item.text}
</motion.span>
{/* Remove button */}
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={onRemove}
className="p-1 text-muted-foreground hover:text-destructive"
>
<X className="h-4 w-4" />
</motion.button>
</Reorder.Item>
);
}
/**
* Page Transition Template
*
* A reusable page transition wrapper for Next.js App Router.
* Provides smooth enter/exit animations between routes.
*
* Usage:
* 1. Use in individual pages:
* ```tsx
* // app/about/page.tsx
* import { PageTransition } from "@/components/page-transition";
*
* export default function AboutPage() {
* return (
* <PageTransition>
* <h1>About</h1>
* <p>Page content...</p>
* </PageTransition>
* );
* }
* ```
*
* 2. Or use in template.tsx for app-wide transitions:
* ```tsx
* // app/template.tsx
* import { PageTransitionProvider } from "@/components/page-transition";
*
* export default function Template({ children }: { children: React.ReactNode }) {
* return <PageTransitionProvider>{children}</PageTransitionProvider>;
* }
* ```
*/
"use client";
import { ReactNode } from "react";
import { AnimatePresence, motion, Variants } from "framer-motion";
import { usePathname } from "next/navigation";
// ============================================================================
// Transition Variants - Choose or customize
// ============================================================================
/**
* Fade transition - Simple opacity change
*/
export const fadeVariants: Variants = {
initial: {
opacity: 0,
},
enter: {
opacity: 1,
transition: {
duration: 0.3,
ease: "easeOut",
},
},
exit: {
opacity: 0,
transition: {
duration: 0.2,
ease: "easeIn",
},
},
};
/**
* Slide up transition - Content slides up while fading
*/
export const slideUpVariants: Variants = {
initial: {
opacity: 0,
y: 20,
},
enter: {
opacity: 1,
y: 0,
transition: {
duration: 0.4,
ease: [0.25, 0.1, 0.25, 1],
},
},
exit: {
opacity: 0,
y: -20,
transition: {
duration: 0.3,
ease: [0.25, 0.1, 0.25, 1],
},
},
};
/**
* Scale transition - Content scales while fading
*/
export const scaleVariants: Variants = {
initial: {
opacity: 0,
scale: 0.98,
},
enter: {
opacity: 1,
scale: 1,
transition: {
duration: 0.4,
ease: [0.25, 0.1, 0.25, 1],
},
},
exit: {
opacity: 0,
scale: 0.98,
transition: {
duration: 0.3,
},
},
};
/**
* Slide with scale - Combined slide and scale effect
*/
export const slideScaleVariants: Variants = {
initial: {
opacity: 0,
y: 30,
scale: 0.98,
},
enter: {
opacity: 1,
y: 0,
scale: 1,
transition: {
duration: 0.5,
ease: [0.25, 0.1, 0.25, 1],
},
},
exit: {
opacity: 0,
y: -20,
scale: 0.98,
transition: {
duration: 0.3,
},
},
};
// ============================================================================
// Page Transition Component
// ============================================================================
interface PageTransitionProps {
children: ReactNode;
/**
* Choose a preset variant or provide custom variants
* @default "slideUp"
*/
variant?: "fade" | "slideUp" | "scale" | "slideScale" | Variants;
/**
* Additional CSS classes for the motion wrapper
*/
className?: string;
}
const variantMap = {
fade: fadeVariants,
slideUp: slideUpVariants,
scale: scaleVariants,
slideScale: slideScaleVariants,
};
/**
* PageTransition - Wrap your page content for enter animations
*
* Note: This only animates enter. For exit animations with route changes,
* use PageTransitionProvider in template.tsx
*/
export function PageTransition({
children,
variant = "slideUp",
className,
}: PageTransitionProps) {
const variants = typeof variant === "string" ? variantMap[variant] : variant;
return (
<motion.div
variants={variants}
initial="initial"
animate="enter"
exit="exit"
className={className}
>
{children}
</motion.div>
);
}
// ============================================================================
// Page Transition Provider (for template.tsx)
// ============================================================================
interface PageTransitionProviderProps {
children: ReactNode;
/**
* Choose a preset variant or provide custom variants
* @default "slideUp"
*/
variant?: "fade" | "slideUp" | "scale" | "slideScale" | Variants;
/**
* AnimatePresence mode
* - "wait": Wait for exit before enter (recommended)
* - "sync": Enter and exit simultaneously
* - "popLayout": Maintain layout during exit
* @default "wait"
*/
mode?: "wait" | "sync" | "popLayout";
/**
* Additional CSS classes for the motion wrapper
*/
className?: string;
}
/**
* PageTransitionProvider - Use in template.tsx for app-wide transitions
*
* Provides AnimatePresence wrapper that enables exit animations
* when navigating between routes.
*/
export function PageTransitionProvider({
children,
variant = "slideUp",
mode = "wait",
className,
}: PageTransitionProviderProps) {
const pathname = usePathname();
const variants = typeof variant === "string" ? variantMap[variant] : variant;
return (
<AnimatePresence mode={mode}>
<motion.div
key={pathname}
variants={variants}
initial="initial"
animate="enter"
exit="exit"
className={className}
>
{children}
</motion.div>
</AnimatePresence>
);
}
// ============================================================================
// Staggered Page Content
// ============================================================================
const staggerContainerVariants: Variants = {
initial: {
opacity: 0,
},
enter: {
opacity: 1,
transition: {
duration: 0.3,
when: "beforeChildren",
staggerChildren: 0.1,
},
},
exit: {
opacity: 0,
transition: {
duration: 0.2,
},
},
};
const staggerItemVariants: Variants = {
initial: {
opacity: 0,
y: 20,
},
enter: {
opacity: 1,
y: 0,
transition: {
duration: 0.4,
ease: [0.25, 0.1, 0.25, 1],
},
},
};
interface StaggeredPageProps {
children: ReactNode;
className?: string;
}
/**
* StaggeredPage - Page wrapper that staggers child animations
*
* Use motion.div with variants={staggerItemVariants} for children
* to get staggered entrance effect.
*
* @example
* ```tsx
* <StaggeredPage>
* <motion.h1 variants={staggerItemVariants}>Title</motion.h1>
* <motion.p variants={staggerItemVariants}>Content</motion.p>
* <motion.div variants={staggerItemVariants}>More content</motion.div>
* </StaggeredPage>
* ```
*/
export function StaggeredPage({ children, className }: StaggeredPageProps) {
return (
<motion.div
variants={staggerContainerVariants}
initial="initial"
animate="enter"
exit="exit"
className={className}
>
{children}
</motion.div>
);
}
// Export the item variants for use in children
export { staggerItemVariants };
Related skills
Forks & variants (1)
Framer Motion has 1 known copy in the catalog totaling 1 installs. They canonicalize to this original listing.
- majiayu000 - 1 installs
FAQ
How do I run exit animations?
Wrap the element in AnimatePresence and provide an 'exit' prop on the motion component.
Does it handle reduced motion?
Yes, it uses useReducedMotion to disable movement when the user prefers reduced motion.