关于 framer-motion
面向使用 Framer Motion 12.x 的 React 开发者的 Web 动画技能。涵盖声明式动画、手势、过渡效果及减少动态效果的无障碍支持。
主要功能
- 01声明式动画组件
- 02复杂序列的变体配置
- 03悬停、点击与拖拽手势
- 04页面与布局过渡
- 05滚动触发动画
- 06减少动态效果支持
适用场景
- 01为 React 交互元素添加动画
- 02编排跨组件动画序列
- 03为页面或状态切换添加过渡
- 04遵循减少动态效果偏好
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ dralgorhythm/ claude-agentic-framework --skill framer-motion安装后在 Claude Code 中输入/framer-motion确认已加载。
详细介绍
Framer Motion
平台: 仅限 Web。移动端动画请参阅 react-native-reanimated 技能。
概述
使用 Framer Motion 12.x 为 React 创建动画模式。提供声明式动画、手势处理、布局过渡和页面动画,并内置性能优化与无障碍支持。
安装:pnpm add framer-motion
工作流
添加动画:
- 导入 motion 组件:
import { motion } from 'framer-motion' - 将元素替换为 motion 变体:
<div>→<motion.div> - 添加动画属性:initial、animate、transition
- 在启用减少动态效果的情况下进行测试
- 在 DevTools 中确认性能达到 60fps
复杂序列:
- 定义带有命名状态的 variants 对象
- 将 variants 应用于父元素和子元素
- 使用编排属性:staggerChildren、delayChildren
- 如果要卸载元素,请用 AnimatePresence 包裹
- 添加无障碍回退方案
动画基元
基础 Motion 组件
import { motion } from 'framer-motion';
// Simple fade-in
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
Content
</motion.div>
// Slide up with fade
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
>
Content
</motion.div>
用于复杂动画的 Variants
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.2
}
},
exit: { opacity: 0, transition: { duration: 0.15 } }
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -20 }
};
<motion.ul variants={containerVariants} initial="hidden" animate="visible" exit="exit">
{items.map(item => (
<motion.li key={item.id} variants={itemVariants}>
{item.name}
</motion.li>
))}
</motion.ul>
过渡
标准时长
// Use consistent timing across app
const timing = {
fast: 0.15, // Micro-interactions
normal: 0.3, // Default animations
slow: 0.5, // Page transitions
stagger: 0.05 // Between items
};
// Duration and easing
<motion.div
animate={{ x: 100 }}
transition={{ duration: timing.normal, ease: 'easeInOut' }}
/>
// Spring physics (preferred for natural motion)
<motion.div
animate={{ scale: 1.2 }}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
/>
// Keyframes
<motion.div
animate={{ scale: [1, 1.2, 1] }}
transition={{ duration: 0.5, times: [0, 0.5, 1] }}
/>
// Repeat
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 2, repeat: Infinity, repeatType: 'loop' }}
/>
手势
悬停、点击、焦点
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
whileFocus={{ outline: '2px solid blue' }}
transition={{ duration: 0.15 }}
>
Click me
</motion.button>
// Complex hover state
<motion.div
initial="rest"
whileHover="hover"
variants={{
rest: { scale: 1, boxShadow: '0 2px 4px rgba(0,0,0,0.1)' },
hover: { scale: 1.02, boxShadow: '0 8px 16px rgba(0,0,0,0.15)' }
}}
>
Card content
</motion.div>
带约束的拖动
import { useRef } from 'react';
const constraintsRef = useRef(null);
<div ref={constraintsRef} style={{ width: 400, height: 400 }}>
<motion.div
drag
dragConstraints={constraintsRef}
dragElastic={0.1}
whileDrag={{ scale: 1.1, cursor: 'grabbing' }}
>
Drag me
</motion.div>
</div>
// Drag along single axis
<motion.div drag="x" dragConstraints={{ left: -100, right: 100 }}>
Slide horizontal
</motion.div>
布局动画
自动布局动画
// Auto-animates position/size changes
<motion.div layout>
{expanded ? <FullContent /> : <Summary />}
</motion.div>
// Shared element transitions
<motion.div layoutId="card-123">
<motion.img layoutId="card-image-123" src={image} />
</motion.div>
// Coordinate sibling animations
import { LayoutGroup } from 'framer-motion';
<LayoutGroup>
{items.map(item => (
<motion.div key={item.id} layout>
{item.content}
</motion.div>
))}
</LayoutGroup>
页面过渡
使用 AnimatePresence 实现退出动画
import { AnimatePresence } from 'framer-motion';
// Single element
<AnimatePresence mode="wait">
{isVisible && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
Content
</motion.div>
)}
</AnimatePresence>
// Route transitions (with React Router)
import { useLocation } from 'react-router-dom';
const location = useLocation();
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={location.pathname}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.3 }}
>
<Routes location={location}>
{/* routes */}
</Routes>
</motion.div>
</AnimatePresence>
交错模式
// Parent orchestration
const listVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.2
}
}
};
const itemVariants = {
hidden: { opacity: 0, x: -20 },
visible: { opacity: 1, x: 0 }
};
<motion.ul variants={listVariants} initial="hidden" animate="visible">
{items.map(item => (
<motion.li key={item.id} variants={itemVariants}>
{item.name}
</motion.li>
))}
</motion.ul>
// Custom stagger with useAnimate
import { useAnimate, stagger } from 'framer-motion';
const [scope, animate] = useAnimate();
useEffect(() => {
animate('.item', { opacity: 1 }, { delay: stagger(0.05) });
}, []);
性能
GPU 加速属性
// ✅ FAST: Only transform and opacity
<motion.div
animate={{
opacity: 1,
scale: 1.2,
x: 100,
rotate: 45
}}
/>
// ❌ SLOW: Layout-affecting properties
<motion.div
animate={{
width: 300, // Triggers layout
height: 200, // Triggers layout
top: 50 // Triggers layout
}}
/>
willChange 优化
// Hint browser before expensive animations
<motion.div
style={{ willChange: 'transform' }}
whileHover={{ scale: 1.1 }}
>
Content
</motion.div>
// Auto willChange with layout animations
<motion.div layout transition={{ layout: { duration: 0.3 } }}>
Content
</motion.div>
无障碍
减少动态效果支持
import { useReducedMotion } from 'framer-motion';
function AnimatedComponent() {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: shouldReduceMotion ? 0 : 0.3 }}
>
Content
</motion.div>
);
}
// Disable animations completely
const prefersReducedMotion = useReducedMotion();
<motion.div
{...(prefersReducedMotion ? {} : {
initial: { opacity: 0 },
animate: { opacity: 1 },
transition: { duration: 0.3 }
})}
>
Content
</motion.div>
焦点管理
// Maintain focus during animations
<AnimatePresence>
{isOpen && (
<motion.dialog
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
onAnimationComplete={() => {
// Focus first input after enter animation
dialogRef.current?.querySelector('input')?.focus();
}}
>
<form>...</form>
</motion.dialog>
)}
</AnimatePresence>
滚动动画
useScroll 和 useInView
import { motion, useScroll, useTransform, useInView } from 'framer-motion';
import { useRef } from 'react';
// Scroll progress indicator
function ScrollProgress() {
const { scrollYProgress } = useScroll();
return (
<motion.div
className="fixed top-0 left-0 right-0 h-1 bg-blue-600 origin-left"
style={{ scaleX: scrollYProgress }}
/>
);
}
// Parallax effect
function ParallaxSection() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"]
});
const y = useTransform(scrollYProgress, [0, 1], [100, -100]);
return (
<div ref={ref}>
<motion.div style={{ y }}>
Parallax content
</motion.div>
</div>
);
}
// Trigger animation when element enters viewport
function AnimateOnScroll({ 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.5 }}
>
{children}
</motion.div>
);
}
MotionConfig
全局动画设置
import { MotionConfig } from 'framer-motion';
// Apply global settings to all descendants
function App() {
return (
<MotionConfig
reducedMotion="user" // Respect prefers-reduced-motion
transition={{ duration: 0.3, ease: "easeOut" }}
>
<YourApp />
</MotionConfig>
);
}
// Override transitions for a section
function FastSection() {
return (
<MotionConfig transition={{ duration: 0.15 }}>
<motion.div animate={{ scale: 1.1 }}>
Uses fast transition
</motion.div>
</MotionConfig>
);
}
最佳实践
- 复杂的多步动画使用 variants,而不是内联对象
- 优先使用弹簧物理效果,而不是基于时长的缓动,以实现自然运动
- 仅为 transform 和 opacity 添加动画,以达到 60fps 性能
- 务必在启用减少动态效果的情况下进行测试(系统设置 → 辅助功能)
- 使用 layoutId 在路由或状态之间实现共享元素过渡
- 使用 AnimatePresence 包裹退出动画,并设置唯一键
- 为频繁播放动画的元素设置 willChange
- 列表动画使用 staggerChildren,而不是手动设置延迟
- 结合使用 layout 和 whileHover,实现动态交互布局
- 将过渡时长控制在 500ms 以内,提升感知性能
反模式
- ❌ 直接为 width/height 添加动画(改用 scale + layout)
- ❌ 条件渲染时忘记使用 AnimatePresence 包裹
- ❌ 将时长值硬编码(使用常量)
- ❌ 忽略 prefers-reduced-motion
- ❌ 为非 GPU 属性添加动画(top、left、width、height、margin)
- ❌ 对每个元素都使用 motion(会给静态内容带来开销)
- ❌ 深度嵌套布局动画(会影响性能)
- ❌ AnimatePresence 的子元素缺少键
- ❌ 不使用 AnimatePresence 就使用 exit
- ❌ 在 SSR 期间播放动画(会导致水合不匹配)
反馈循环
动画质量:
# Check frame rate in Chrome DevTools
# Performance → Record → Look for dropped frames
# Target: 60fps (16.67ms per frame)
减少动态效果测试:
# macOS: System Settings → Accessibility → Display → Reduce Motion
# Test all animations with this enabled
性能分析:
// Use React DevTools Profiler
// Measure commit duration with/without animations
// Aim for <16ms commits
评论
支持 Markdown