animations
SKILL.md
Animations Skill
This skill guides the creation of beautiful, purposeful animations using Framer Motion. Great animations feel natural, add delight, and improve user experience.
When to Use This Skill
- Adding hover states and micro-interactions
- Creating page transitions
- Animating elements on scroll
- Building loading states and skeletons
- Creating staggered list animations
- Adding entrance animations to sections
- Building interactive UI elements
Philosophy
Animations should be:
- Purposeful - Every animation communicates something
- Subtle - Enhancement, not distraction
- Fast - Users shouldn't wait for animations
- Consistent - Same timing and easing throughout
Avoid:
- Animations that block user action
- Motion for motion's sake
- Jarring or unexpected movements
- Inconsistent timing across the site
Setup
Install Framer Motion
npm install framer-motion
Basic Import
"use client";
import { motion } from "framer-motion";
Timing & Easing Reference
Standard Durations
| Type | Duration | Use Case |
|---|---|---|
| Micro | 0.1-0.15s | Button hovers, icon changes |
| Fast | 0.2-0.3s | Fade ins, small movements |
| Medium | 0.3-0.5s | Page elements, cards |
| Slow | 0.5-0.8s | Page transitions, large elements |
Recommended Easings
// Smooth and natural
const easeOut = [0.33, 1, 0.68, 1];
// Snappy entrance
const easeOutBack = [0.34, 1.56, 0.64, 1];
// Elegant deceleration
const easeOutExpo = [0.16, 1, 0.3, 1];
// Springy feel
const spring = { type: "spring", stiffness: 300, damping: 30 };
Animation Patterns
1. Fade In on Mount
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
Content
</motion.div>
2. Fade Up on Mount
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: [0.33, 1, 0.68, 1] }}
>
Content
</motion.div>
3. Staggered Children
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((item) => (
<motion.li key={item.id} variants={item}>
{item.content}
</motion.li>
))}
</motion.ul>
4. Hover Scale
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
transition={{ duration: 0.15 }}
>
Click me
</motion.button>
5. Hover with Background Change
<motion.a
className="relative px-4 py-2 overflow-hidden"
whileHover="hover"
>
<motion.span
className="absolute inset-0 bg-black/5"
initial={{ scale: 0 }}
variants={{ hover: { scale: 1 } }}
transition={{ duration: 0.3 }}
/>
<span className="relative">Link text</span>
</motion.a>
6. Scroll-Triggered Animation
import { motion, useInView } from "framer-motion";
import { useRef } from "react";
function Section({ children }) {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
return (
<motion.section
ref={ref}
initial={{ opacity: 0, y: 40 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease: [0.33, 1, 0.68, 1] }}
>
{children}
</motion.section>
);
}
7. Page Transition Wrapper
// components/PageTransition.tsx
"use client";
import { motion } from "framer-motion";
export function PageTransition({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3, ease: [0.33, 1, 0.68, 1] }}
>
{children}
</motion.div>
);
}
8. Expandable Card
<motion.div
layout
className="bg-white rounded-lg p-4"
onClick={() => setExpanded(!expanded)}
>
<motion.h3 layout="position">Title</motion.h3>
{expanded && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
Expanded content here
</motion.p>
)}
</motion.div>
9. Loading Skeleton
<motion.div
className="h-4 bg-gray-200 rounded"
animate={{ opacity: [0.5, 1, 0.5] }}
transition={{ duration: 1.5, repeat: Infinity }}
/>
10. Menu/Dropdown
import { AnimatePresence, motion } from "framer-motion";
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
className="absolute top-full mt-2 bg-white shadow-lg rounded-lg"
>
{/* Menu content */}
</motion.div>
)}
</AnimatePresence>
Reusable Components
FadeIn Component
"use client";
import { motion, Variants } from "framer-motion";
import { ReactNode } from "react";
interface FadeInProps {
children: ReactNode;
delay?: number;
direction?: "up" | "down" | "left" | "right";
className?: string;
}
export function FadeIn({
children,
delay = 0,
direction = "up",
className,
}: FadeInProps) {
const directions = {
up: { y: 20 },
down: { y: -20 },
left: { x: 20 },
right: { x: -20 },
};
return (
<motion.div
initial={{ opacity: 0, ...directions[direction] }}
animate={{ opacity: 1, x: 0, y: 0 }}
transition={{
duration: 0.5,
delay,
ease: [0.33, 1, 0.68, 1],
}}
className={className}
>
{children}
</motion.div>
);
}
StaggeredList Component
"use client";
import { motion } from "framer-motion";
import { ReactNode } from "react";
interface StaggeredListProps {
children: ReactNode[];
staggerDelay?: number;
className?: string;
}
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const item = {
hidden: { opacity: 0, y: 20 },
show: {
opacity: 1,
y: 0,
transition: {
duration: 0.4,
ease: [0.33, 1, 0.68, 1],
},
},
};
export function StaggeredList({
children,
staggerDelay = 0.1,
className,
}: StaggeredListProps) {
return (
<motion.div
variants={{
...container,
show: {
...container.show,
transition: { staggerChildren: staggerDelay },
},
}}
initial="hidden"
animate="show"
className={className}
>
{children.map((child, index) => (
<motion.div key={index} variants={item}>
{child}
</motion.div>
))}
</motion.div>
);
}
AnimatedButton Component
"use client";
import { motion } from "framer-motion";
import { ButtonHTMLAttributes, ReactNode } from "react";
interface AnimatedButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
variant?: "primary" | "secondary";
}
export function AnimatedButton({
children,
variant = "primary",
className,
...props
}: AnimatedButtonProps) {
const baseClasses =
variant === "primary"
? "bg-black text-white"
: "bg-white text-black border border-black/10";
return (
<motion.button
className={`px-6 py-3 rounded-lg font-medium ${baseClasses} ${className}`}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
transition={{ duration: 0.15 }}
{...props}
>
{children}
</motion.button>
);
}
Section-Specific Animations
Hero Section
// Stagger headline, subheadline, and CTA
const heroVariants = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.15, delayChildren: 0.2 },
},
};
const heroItemVariants = {
hidden: { opacity: 0, y: 30 },
show: {
opacity: 1,
y: 0,
transition: { duration: 0.6, ease: [0.33, 1, 0.68, 1] },
},
};
Feature Cards
// Stagger cards on scroll
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-50px" });
<motion.div
ref={ref}
variants={containerVariants}
initial="hidden"
animate={isInView ? "show" : "hidden"}
className="grid grid-cols-3 gap-6"
>
{features.map((feature) => (
<motion.div key={feature.id} variants={itemVariants}>
{/* Card content */}
</motion.div>
))}
</motion.div>
Testimonials
// Subtle float animation for quotes
<motion.blockquote
animate={{ y: [0, -5, 0] }}
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }}
>
{quote}
</motion.blockquote>
Performance Tips
-
Use
layoutsparingly - It can be expensive. Only use when necessary. -
Prefer
transformandopacity- These are GPU-accelerated:// Good animate={{ x: 10, opacity: 0.5 }} // Avoid animate={{ left: 10, width: 100 }} -
Set
will-changefor complex animations:<motion.div style={{ willChange: "transform" }} /> -
Use
useReducedMotionfor accessibility:import { useReducedMotion } from "framer-motion"; function Component() { const shouldReduceMotion = useReducedMotion(); return ( <motion.div animate={{ y: shouldReduceMotion ? 0 : 20 }} /> ); } -
Avoid animating on scroll without throttling - Use
useInViewwithonce: true.
Checklist Before Shipping
- Animations are fast (under 500ms for most)
- Consistent easing across the site
- Respects
prefers-reduced-motion - No layout shifts during animation
- Mobile performance tested
- Exit animations added where needed (AnimatePresence)
- Stagger delays feel natural, not too slow
Weekly Installs
2
Repository
ship-studio/sau…templateFirst Seen
Feb 21, 2026
Security Audits
Installed on
gemini-cli2
opencode2
antigravity2
command-code2
github-copilot2
codex2