Framer Motion Animations
Installation
npm install framer-motion
Basic Fade In
'use client';
import { motion } from 'framer-motion';
export function FadeIn({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
{children}
</motion.div>
);
}
Staggered Children
'use client';
import { motion } from 'framer-motion';
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 },
};
export function StaggeredList({ items }: { items: React.ReactNode[] }) {
return (
<motion.ul
variants={container}
initial="hidden"
animate="show"
className="space-y-4"
>
{items.map((child, i) => (
<motion.li key={i} variants={item}>
{child}
</motion.li>
))}
</motion.ul>
);
}
Scroll-Triggered Animation
'use client';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
export function ScrollReveal({ children }: { children: React.ReactNode }) {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 50 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }}
transition={{ duration: 0.6, ease: 'easeOut' }}
>
{children}
</motion.div>
);
}
Hero Animation
'use client';
import { motion } from 'framer-motion';
export function HeroSection() {
return (
<section className="min-h-screen flex items-center">
<div className="container mx-auto px-4">
<motion.h1
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
className="text-5xl md:text-7xl font-bold"
>
Pilates & Yoga
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
className="text-xl mt-6 text-muted-foreground"
>
Transform your body and mind
</motion.p>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.6 }}
className="mt-8 flex gap-4"
>
<Button>Book Now</Button>
<Button variant="outline">Learn More</Button>
</motion.div>
</div>
</section>
);
}
Card Hover Effect
'use client';
import { motion } from 'framer-motion';
export function AnimatedCard({ children }: { children: React.ReactNode }) {
return (
<motion.div
whileHover={{ y: -5, boxShadow: '0 10px 30px rgba(0,0,0,0.1)' }}
transition={{ duration: 0.2 }}
className="bg-card rounded-lg p-6 border"
>
{children}
</motion.div>
);
}
Section Wrapper
'use client';
import { motion } from 'framer-motion';
import { useInView } from 'framer-motion';
import { useRef } from 'react';
interface SectionProps {
children: React.ReactNode;
className?: string;
delay?: number;
}
export function AnimatedSection({ children, className, delay = 0 }: SectionProps) {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: '-50px' });
return (
<motion.section
ref={ref}
initial={{ opacity: 0, y: 40 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay, ease: 'easeOut' }}
className={className}
>
{children}
</motion.section>
);
}
Counter Animation
'use client';
import { motion, useMotionValue, useTransform, animate } from 'framer-motion';
import { useEffect } from 'react';
interface CounterProps {
from: number;
to: number;
duration?: number;
}
export function AnimatedCounter({ from, to, duration = 2 }: CounterProps) {
const count = useMotionValue(from);
const rounded = useTransform(count, (latest) => Math.round(latest));
useEffect(() => {
const controls = animate(count, to, { duration });
return controls.stop;
}, [count, to, duration]);
return <motion.span>{rounded}</motion.span>;
}
// Usage
<div className="text-4xl font-bold">
<AnimatedCounter from={0} to={500} />+
<span className="text-sm">Happy Clients</span>
</div>
Page Transition Wrapper
'use client';
import { motion, AnimatePresence } from 'framer-motion';
import { usePathname } from 'next/navigation';
export function PageTransition({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
return (
<AnimatePresence mode="wait">
<motion.div
key={pathname}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
</AnimatePresence>
);
}
Accordion Animation
'use client';
import { motion, AnimatePresence } from 'framer-motion';
import { useState } from 'react';
interface AccordionItemProps {
title: string;
children: React.ReactNode;
}
export function AccordionItem({ title, children }: AccordionItemProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="border-b">
<button
onClick={() => setIsOpen(!isOpen)}
className="w-full py-4 flex justify-between items-center"
>
<span className="font-medium">{title}</span>
<motion.span
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
▼
</motion.span>
</button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3 }}
className="overflow-hidden"
>
<div className="pb-4">{children}</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
Performance Tips
- Use
layoutId for shared element transitions
- Use
useReducedMotion() to respect user preferences
- Avoid animating layout properties (width, height) - use transforms
- Use
will-change sparingly via Tailwind's will-change-transform
- Prefer
opacity and transform animations (GPU accelerated)