framer-motion-animator
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFramer Motion Animator
Framer Motion 动画实现指南
Build delightful animations and interactions with Framer Motion's declarative API.
使用Framer Motion的声明式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
- 确定动画需求:入场、退场、悬停、手势
- 选择动画类型:简单动画、变体、手势、布局
- 定义运动值:透明度、缩放、位置、旋转
- 添加过渡效果:时长、缓动、弹簧物理
- 编排动画序列:交错、延迟、父子联动
- 优化性能:使用GPU加速属性
Installation
安装
bash
npm install framer-motionbash
npm install framer-motionBasic Animations
基础动画
Simple Animation
简单动画
tsx
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>
);
}tsx
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>
);
}
// 悬停时动画
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
结合AnimatePresence实现退场动画
tsx
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>
);
}tsx
import { motion, AnimatePresence } from 'framer-motion';
export function Modal({ isOpen, onClose, children }: ModalProps) {
return (
<AnimatePresence>
{isOpen && (
<>
{/* 遮罩层 */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="fixed inset-0 bg-black/50 z-40"
/>
{/* 弹窗 */}
<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
子元素交错动画
tsx
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>
);
}tsx
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
交互式变体
tsx
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>
);
}tsx
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
Next.js App Router 实现
tsx
// 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>
);
}tsx
// 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
共享布局动画
tsx
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>
);
}tsx
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
拖拽动画
tsx
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"
/>
);
}tsx
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
滑动删除
tsx
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>
);
}tsx
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
滚动触发动画
tsx
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>
);
}tsx
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
滚动进度动画
tsx
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"
/>
);
}tsx
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)
useAnimate(命令式)
tsx
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>
);
}tsx
import { useAnimate } from 'framer-motion';
export function SubmitButton() {
const [scope, animate] = useAnimate();
const handleClick = async () => {
// 动画序列
await animate(scope.current, { scale: 0.95 }, { duration: 0.1 });
await animate(scope.current, { scale: 1 }, { type: 'spring' });
// 成功状态动画
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
useMotionValue & useTransform
tsx
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"
/>
);
}tsx
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
AnimatedContainer
tsx
// 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>
);
}tsx
// 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
AnimatedList
tsx
// 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>
);
}tsx
// 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
过渡预设
tsx
// 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} />tsx
// 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;
// 使用示例
<motion.div transition={transitions.spring} />Reduced Motion Support
精简动画支持
tsx
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>
);
}tsx
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 animations
layout - Use : For exit animations
AnimatePresence - Prefer springs: More natural than tween for UI
- Respect reduced motion: Use hook
useReducedMotion - Avoid animating layout thrashing: Don't animate ,
top,leftwidth - Use : For shared element transitions
layoutId - Stagger children: For list animations
- 使用GPU加速属性:、
opacity(避免transform、width)height - 添加实现流畅尺寸变化:自动布局动画
layout - 使用:实现退场动画
AnimatePresence - 优先使用弹簧动画:比补间动画更符合UI自然交互
- 尊重精简动画设置:使用钩子
useReducedMotion - 避免触发布局抖动的动画:不要动画、
top、leftwidth - 使用:实现共享元素过渡
layoutId - 子元素交错动画:适用于列表动画
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
每个动画实现都应包含:
- 合适的动画类型(简单、变体、手势)
- 带有恰当缓动效果的流畅过渡
- 使用AnimatePresence实现退场动画
- 支持精简动画设置
- 仅使用GPU加速属性
- 弹簧物理效果带来自然体验
- 列表使用子元素交错动画
- 在低端设备上测试性能