Claude-skill-registry animation-motion
Create smooth animations and micro-interactions with Framer Motion and CSS. Covers enter/exit animations, gestures, scroll animations, loading states, and performance optimization. Use for polished UIs, interactive elements, and engaging user experiences.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/animation-motion" ~/.claude/skills/majiayu000-claude-skill-registry-animation-motion && rm -rf "$T"
manifest:
skills/data/animation-motion/SKILL.mdsource content
Animation & Motion Design
Create smooth, purposeful animations that enhance user experience.
Instructions
- Animate with purpose - Motion should guide, not distract
- Keep it fast - Most UI animations should be 150-300ms
- Use easing curves - Never use linear timing for UI
- Respect preferences - Honor
prefers-reduced-motion - Optimize performance - Animate
andtransform
onlyopacity
Framer Motion (Recommended)
Setup
npm install framer-motion
Basic Animations
import { motion } from 'framer-motion'; // Fade in on mount function FadeIn({ children }: { children: React.ReactNode }) { return ( <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.3 }} > {children} </motion.div> ); } // Slide up on mount function SlideUp({ children }: { children: React.ReactNode }) { return ( <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4, ease: 'easeOut' }} > {children} </motion.div> ); } // Scale on hover function ScaleButton({ children }: { children: React.ReactNode }) { return ( <motion.button whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} transition={{ type: 'spring', stiffness: 400, damping: 17 }} > {children} </motion.button> ); }
Enter/Exit Animations
import { motion, AnimatePresence } from 'framer-motion'; function Modal({ isOpen, onClose, children }: ModalProps) { return ( <AnimatePresence> {isOpen && ( <> {/* Backdrop */} <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} className="fixed inset-0 bg-black/50" onClick={onClose} /> {/* 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={{ duration: 0.2, ease: 'easeOut' }} className="fixed inset-0 flex items-center justify-center p-4" > <div className="bg-white rounded-xl shadow-xl max-w-md w-full p-6"> {children} </div> </motion.div> </> )} </AnimatePresence> ); }
List Animations
import { motion, AnimatePresence } 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 }, }; function AnimatedList({ items }: { items: Item[] }) { return ( <motion.ul variants={container} initial="hidden" animate="show"> <AnimatePresence mode="popLayout"> {items.map((item) => ( <motion.li key={item.id} variants={item} exit={{ opacity: 0, x: -100 }} layout > <ItemCard {...item} /> </motion.li> ))} </AnimatePresence> </motion.ul> ); }
Layout Animations
import { motion, LayoutGroup } from 'framer-motion'; function ExpandableCard({ id, title, content, isExpanded, onToggle }: Props) { return ( <LayoutGroup> <motion.div layout onClick={onToggle} className="bg-white rounded-xl p-4 cursor-pointer" > <motion.h3 layout="position" className="font-semibold"> {title} </motion.h3> <AnimatePresence> {isExpanded && ( <motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} exit={{ opacity: 0, height: 0 }} transition={{ duration: 0.3 }} > <p className="mt-4 text-gray-600">{content}</p> </motion.div> )} </AnimatePresence> </motion.div> </LayoutGroup> ); }
Scroll Animations
import { motion, useScroll, useTransform } from 'framer-motion'; function ParallaxHero() { const { scrollY } = useScroll(); // Parallax effect - image moves slower than scroll const y = useTransform(scrollY, [0, 500], [0, 150]); const opacity = useTransform(scrollY, [0, 300], [1, 0]); return ( <div className="relative h-screen overflow-hidden"> <motion.div style={{ y }} className="absolute inset-0" > <img src="/hero.jpg" className="w-full h-full object-cover" /> </motion.div> <motion.div style={{ opacity }} className="relative z-10 flex items-center justify-center h-full" > <h1 className="text-6xl font-bold text-white">Welcome</h1> </motion.div> </div> ); } // Reveal on scroll 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.6, ease: 'easeOut' }} > {children} </motion.div> ); }
Gesture Animations
import { motion, useDragControls } from 'framer-motion'; function DraggableCard() { return ( <motion.div drag dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }} dragElastic={0.1} whileDrag={{ scale: 1.05, cursor: 'grabbing' }} className="w-48 h-48 bg-blue-500 rounded-xl cursor-grab" /> ); } function SwipeToDelete({ onDelete }: { onDelete: () => void }) { return ( <motion.div drag="x" dragConstraints={{ left: 0, right: 0 }} onDragEnd={(_, info) => { if (info.offset.x < -100) { onDelete(); } }} className="bg-white p-4 rounded-lg" > Swipe left to delete </motion.div> ); }
Loading States
Skeleton Loader
function Skeleton({ className = '' }: { className?: string }) { return ( <div className={`animate-pulse bg-gray-200 dark:bg-gray-700 rounded ${className}`} /> ); } function CardSkeleton() { return ( <div className="bg-white rounded-xl p-6 space-y-4"> <Skeleton className="h-6 w-3/4" /> <Skeleton className="h-4 w-full" /> <Skeleton className="h-4 w-5/6" /> <div className="flex gap-4 pt-4"> <Skeleton className="h-10 w-24" /> <Skeleton className="h-10 w-24" /> </div> </div> ); }
Spinner
function Spinner({ size = 'md' }: { size?: 'sm' | 'md' | 'lg' }) { const sizes = { sm: 'w-4 h-4', md: 'w-8 h-8', lg: 'w-12 h-12', }; return ( <svg className={`animate-spin text-blue-600 ${sizes[size]}`} viewBox="0 0 24 24" fill="none" > <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /> <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" /> </svg> ); }
Progress Bar
function ProgressBar({ value, max = 100 }: { value: number; max?: number }) { const percentage = Math.min((value / max) * 100, 100); return ( <div className="h-2 bg-gray-200 rounded-full overflow-hidden"> <motion.div className="h-full bg-blue-600" initial={{ width: 0 }} animate={{ width: `${percentage}%` }} transition={{ duration: 0.5, ease: 'easeOut' }} /> </div> ); }
CSS Animations
Tailwind Animations
// Built-in Tailwind animations <div className="animate-spin">Spinner</div> <div className="animate-ping">Ping</div> <div className="animate-pulse">Pulse</div> <div className="animate-bounce">Bounce</div> // Custom animation in tailwind.config.js module.exports = { theme: { extend: { animation: { 'fade-in': 'fadeIn 0.3s ease-out', 'slide-up': 'slideUp 0.4s ease-out', 'scale-in': 'scaleIn 0.2s ease-out', }, keyframes: { fadeIn: { '0%': { opacity: '0' }, '100%': { opacity: '1' }, }, slideUp: { '0%': { transform: 'translateY(20px)', opacity: '0' }, '100%': { transform: 'translateY(0)', opacity: '1' }, }, scaleIn: { '0%': { transform: 'scale(0.95)', opacity: '0' }, '100%': { transform: 'scale(1)', opacity: '1' }, }, }, }, }, };
CSS Transitions
<button className=" bg-blue-600 text-white px-4 py-2 rounded-lg transition-all duration-200 ease-out hover:bg-blue-700 hover:shadow-lg hover:-translate-y-0.5 active:translate-y-0 active:shadow-md "> Hover Me </button>
Respecting User Preferences
import { useReducedMotion } from 'framer-motion'; function AnimatedComponent() { const shouldReduceMotion = useReducedMotion(); return ( <motion.div initial={shouldReduceMotion ? false : { opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.4 }} > Content </motion.div> ); } // CSS approach @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
Performance Tips
- Animate only transform and opacity - GPU accelerated
- Use
sparingly - Only for complex animationswill-change - Avoid layout thrashing - Don't animate width/height
- Use
prop carefully - Can cause reflowslayout - Debounce scroll handlers - Prevent jank
// Good - GPU accelerated <motion.div animate={{ x: 100, opacity: 0.5 }} /> // Bad - causes reflow <motion.div animate={{ width: 200, marginLeft: 100 }} />
When to Use
- Page transitions and navigation
- Loading and skeleton states
- Interactive UI elements
- Feedback and confirmations
- Onboarding and tutorials
- Data visualization transitions
Notes
- Framer Motion adds ~30kb to bundle (gzipped)
- Use CSS for simple hover/focus transitions
- Test animations at 0.25x speed to verify smoothness
- Consider motion sickness - avoid excessive movement