Claude-skill-registry framer-motion-animator
Creates smooth animations and micro-interactions using Framer Motion including page transitions, gestures, scroll-based animations, and orchestrated sequences. Use when users request "add animation", "framer motion", "page transition", "animate component", or "micro-interactions".
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/framer-motion-animator" ~/.claude/skills/majiayu000-claude-skill-registry-framer-motion-animator && rm -rf "$T"
manifest:
skills/data/framer-motion-animator/SKILL.mdsource content
Framer Motion Animator
Build delightful animations and interactions with Framer Motion's declarative API.
Core Workflow
- Identify animation needs: Entrance, exit, hover, gestures
- Choose animation type: Simple, variants, gestures, layout
- Define motion values: Opacity, scale, position, rotation
- Add transitions: Duration, easing, spring physics
- Orchestrate sequences: Stagger, delay, parent-child
- Optimize performance: GPU-accelerated properties
Installation
npm install framer-motion
Basic Animations
Simple Animation
import { motion } from 'framer-motion'; // Animate on mount 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> ); } // Animate on hover export function ScaleOnHover({ children }: { children: React.ReactNode }) { return ( <motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} transition={{ type: 'spring', stiffness: 400, damping: 17 }} > {children} </motion.div> ); }
Exit Animations with AnimatePresence
import { motion, AnimatePresence } from 'framer-motion'; export function Modal({ isOpen, onClose, children }: ModalProps) { 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 inset-0 z-50 flex items-center justify-center" > <div className="bg-white rounded-xl p-6 max-w-md w-full"> {children} </div> </motion.div> </> )} </AnimatePresence> ); }
Variants Pattern
Staggered Children
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" > {items.map((item, index) => ( <motion.li key={index} variants={itemVariants}> {item} </motion.li> ))} </motion.ul> ); }
Interactive Variants
const buttonVariants = { initial: { scale: 1 }, hover: { scale: 1.05 }, tap: { scale: 0.95 }, disabled: { opacity: 0.5, scale: 1 }, }; export function AnimatedButton({ children, disabled, onClick, }: ButtonProps) { return ( <motion.button variants={buttonVariants} initial="initial" whileHover={disabled ? 'disabled' : 'hover'} whileTap={disabled ? 'disabled' : 'tap'} animate={disabled ? 'disabled' : 'initial'} onClick={onClick} disabled={disabled} className="px-4 py-2 bg-blue-500 text-white rounded-lg" > {children} </motion.button> ); }
Page Transitions
Next.js App Router
// app/template.tsx 'use client'; import { motion } from 'framer-motion'; export default function Template({ children }: { children: React.ReactNode }) { return ( <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 20 }} transition={{ duration: 0.3 }} > {children} </motion.div> ); }
Shared Layout Animations
import { motion, LayoutGroup } from 'framer-motion'; export function Tabs({ tabs, activeTab, onTabChange }: TabsProps) { return ( <LayoutGroup> <div className="flex gap-2"> {tabs.map((tab) => ( <button key={tab.id} onClick={() => onTabChange(tab.id)} className="relative px-4 py-2" > {activeTab === tab.id && ( <motion.div layoutId="activeTab" className="absolute inset-0 bg-blue-500 rounded-lg" transition={{ type: 'spring', stiffness: 500, damping: 30 }} /> )} <span className="relative z-10">{tab.label}</span> </button> ))} </div> </LayoutGroup> ); }
Gesture Animations
Drag
export function DraggableCard() { return ( <motion.div drag dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }} dragElastic={0.2} dragTransition={{ bounceStiffness: 600, bounceDamping: 20 }} whileDrag={{ scale: 1.1, cursor: 'grabbing' }} className="w-32 h-32 bg-blue-500 rounded-lg cursor-grab" /> ); }
Swipe to Dismiss
export function SwipeToDelete({ onDelete, children }: SwipeProps) { return ( <motion.div drag="x" dragConstraints={{ left: 0, right: 0 }} onDragEnd={(_, info) => { if (info.offset.x < -100) { onDelete(); } }} className="relative" > {children} <motion.div className="absolute right-0 inset-y-0 bg-red-500 flex items-center px-4" style={{ opacity: 0 }} animate={{ opacity: 1 }} > Delete </motion.div> </motion.div> ); }
Scroll Animations
Scroll-Triggered
import { motion, useInView } from 'framer-motion'; import { useRef } from 'react'; export function FadeInWhenVisible({ 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> ); }
Scroll Progress
import { motion, useScroll, useTransform } from 'framer-motion'; export function ParallaxHero() { const { scrollY } = useScroll(); const y = useTransform(scrollY, [0, 500], [0, 150]); const opacity = useTransform(scrollY, [0, 300], [1, 0]); return ( <motion.div style={{ y, opacity }} className="h-screen flex items-center justify-center" > <h1 className="text-6xl font-bold">Parallax Hero</h1> </motion.div> ); } export function ScrollProgress() { const { scrollYProgress } = useScroll(); return ( <motion.div style={{ scaleX: scrollYProgress }} className="fixed top-0 left-0 right-0 h-1 bg-blue-500 origin-left z-50" /> ); }
Animation Hooks
useAnimate (Imperative)
import { useAnimate } from 'framer-motion'; export function SubmitButton() { const [scope, animate] = useAnimate(); const handleClick = async () => { // Sequence of animations await animate(scope.current, { scale: 0.95 }, { duration: 0.1 }); await animate(scope.current, { scale: 1 }, { type: 'spring' }); // Success animation await animate( scope.current, { backgroundColor: '#22c55e' }, { duration: 0.2 } ); }; return ( <motion.button ref={scope} onClick={handleClick} className="px-4 py-2"> Submit </motion.button> ); }
useMotionValue & useTransform
import { motion, useMotionValue, useTransform } from 'framer-motion'; export function RotatingCard() { const x = useMotionValue(0); const rotateY = useTransform(x, [-200, 200], [-45, 45]); const opacity = useTransform(x, [-200, 0, 200], [0.5, 1, 0.5]); return ( <motion.div drag="x" dragConstraints={{ left: -200, right: 200 }} style={{ x, rotateY, opacity }} className="w-64 h-96 bg-gradient-to-br from-purple-500 to-pink-500 rounded-xl" /> ); }
Reusable Animation Components
AnimatedContainer
// components/AnimatedContainer.tsx import { motion, Variants } from 'framer-motion'; const animations: Record<string, Variants> = { fadeIn: { hidden: { opacity: 0 }, visible: { opacity: 1 }, }, fadeInUp: { hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0 }, }, fadeInDown: { hidden: { opacity: 0, y: -20 }, visible: { opacity: 1, y: 0 }, }, scaleIn: { hidden: { opacity: 0, scale: 0.8 }, visible: { opacity: 1, scale: 1 }, }, slideInLeft: { hidden: { opacity: 0, x: -50 }, visible: { opacity: 1, x: 0 }, }, slideInRight: { hidden: { opacity: 0, x: 50 }, visible: { opacity: 1, x: 0 }, }, }; interface AnimatedContainerProps { children: React.ReactNode; animation?: keyof typeof animations; delay?: number; duration?: number; className?: string; } export function AnimatedContainer({ children, animation = 'fadeInUp', delay = 0, duration = 0.5, className, }: AnimatedContainerProps) { return ( <motion.div variants={animations[animation]} initial="hidden" animate="visible" transition={{ duration, delay, ease: 'easeOut' }} className={className} > {children} </motion.div> ); }
AnimatedList
// components/AnimatedList.tsx import { motion } from 'framer-motion'; const containerVariants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.05, }, }, }; const itemVariants = { hidden: { opacity: 0, x: -20 }, visible: { opacity: 1, x: 0 }, }; interface AnimatedListProps<T> { items: T[]; renderItem: (item: T, index: number) => React.ReactNode; keyExtractor: (item: T, index: number) => string; className?: string; } export function AnimatedList<T>({ items, renderItem, keyExtractor, className, }: AnimatedListProps<T>) { return ( <motion.ul variants={containerVariants} initial="hidden" animate="visible" className={className} > {items.map((item, index) => ( <motion.li key={keyExtractor(item, index)} variants={itemVariants}> {renderItem(item, index)} </motion.li> ))} </motion.ul> ); }
Transition Presets
// lib/transitions.ts export const transitions = { spring: { type: 'spring', stiffness: 300, damping: 24, }, springBouncy: { type: 'spring', stiffness: 500, damping: 15, }, springStiff: { type: 'spring', stiffness: 700, damping: 30, }, smooth: { type: 'tween', duration: 0.3, ease: 'easeInOut', }, snappy: { type: 'tween', duration: 0.15, ease: [0.25, 0.1, 0.25, 1], }, } as const; // Usage <motion.div transition={transitions.spring} />
Reduced Motion Support
import { useReducedMotion } from 'framer-motion'; export function AccessibleAnimation({ children }: { children: React.ReactNode }) { const shouldReduceMotion = useReducedMotion(); return ( <motion.div initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: shouldReduceMotion ? 0 : 0.5 }} > {children} </motion.div> ); }
Best Practices
- Use GPU-accelerated properties:
,opacity
(nottransform
,width
)height - Add
for smooth resizing: Automatic layout animationslayout - Use
: For exit animationsAnimatePresence - Prefer springs: More natural than tween for UI
- Respect reduced motion: Use
hookuseReducedMotion - Avoid animating layout thrashing: Don't animate
,top
,leftwidth - Use
: For shared element transitionslayoutId - Stagger children: For list animations
Output Checklist
Every animation implementation should include:
- Appropriate animation type (simple, variants, gestures)
- Smooth transitions with proper easing
- Exit animations with AnimatePresence
- Reduced motion support
- GPU-accelerated properties only
- Spring physics for natural feel
- Staggered children for lists
- Performance tested on low-end devices