Appearance
动画变体(Variants)
概述
Variants是Framer Motion中最强大的功能之一,它允许你预定义动画状态并在组件间协调动画。本文将深入讲解Variants的概念、用法、高级技巧以及实战应用,帮助你创建复杂而优雅的动画效果。
Variants基础
什么是Variants
Variants是预定义的动画状态对象,可以在父子组件之间传播,实现协调动画。
tsx
import { motion } from 'framer-motion';
const boxVariants = {
hidden: {
opacity: 0,
scale: 0.8,
},
visible: {
opacity: 1,
scale: 1,
},
};
function BasicVariant() {
return (
<motion.div
variants={boxVariants}
initial="hidden"
animate="visible"
>
Animated Box
</motion.div>
);
}多状态Variants
tsx
const cardVariants = {
initial: {
opacity: 0,
y: 50,
},
animate: {
opacity: 1,
y: 0,
},
hover: {
scale: 1.05,
transition: { duration: 0.3 },
},
tap: {
scale: 0.95,
},
exit: {
opacity: 0,
y: -50,
},
};
function MultiStateVariant() {
return (
<motion.div
variants={cardVariants}
initial="initial"
animate="animate"
whileHover="hover"
whileTap="tap"
exit="exit"
>
Interactive Card
</motion.div>
);
}父子动画协调
基础传播
tsx
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
delayChildren: 0.3,
staggerChildren: 0.2
}
}
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1
}
};
function PropagationExample() {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
<motion.li variants={itemVariants}>Item 1</motion.li>
<motion.li variants={itemVariants}>Item 2</motion.li>
<motion.li variants={itemVariants}>Item 3</motion.li>
</motion.ul>
);
}交错动画
tsx
const listVariants = {
hidden: {
opacity: 0
},
visible: {
opacity: 1,
transition: {
when: "beforeChildren",
staggerChildren: 0.1,
}
}
};
const itemVariants = {
hidden: {
opacity: 0,
x: -20
},
visible: {
opacity: 1,
x: 0,
transition: {
type: "spring",
stiffness: 100
}
}
};
function StaggeredList() {
const items = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'];
return (
<motion.ul
variants={listVariants}
initial="hidden"
animate="visible"
className="staggered-list"
>
{items.map((item, index) => (
<motion.li key={index} variants={itemVariants}>
{item}
</motion.li>
))}
</motion.ul>
);
}嵌套Variants
tsx
const menuVariants = {
open: {
opacity: 1,
height: "auto",
transition: {
when: "beforeChildren",
staggerChildren: 0.1
}
},
closed: {
opacity: 0,
height: 0,
transition: {
when: "afterChildren",
staggerChildren: 0.05,
staggerDirection: -1
}
}
};
const menuItemVariants = {
open: {
y: 0,
opacity: 1,
transition: {
y: { stiffness: 1000, velocity: -100 }
}
},
closed: {
y: 50,
opacity: 0,
transition: {
y: { stiffness: 1000 }
}
}
};
const submenuVariants = {
open: {
opacity: 1,
transition: { staggerChildren: 0.07, delayChildren: 0.2 }
},
closed: {
opacity: 0,
transition: { staggerChildren: 0.05, staggerDirection: -1 }
}
};
function NestedMenu() {
const [isOpen, setIsOpen] = useState(false);
return (
<motion.nav
initial={false}
animate={isOpen ? "open" : "closed"}
>
<button onClick={() => setIsOpen(!isOpen)}>Toggle Menu</button>
<motion.ul variants={menuVariants}>
<motion.li variants={menuItemVariants}>
Home
</motion.li>
<motion.li variants={menuItemVariants}>
<span>Products</span>
<motion.ul variants={submenuVariants}>
<motion.li variants={menuItemVariants}>Product 1</motion.li>
<motion.li variants={menuItemVariants}>Product 2</motion.li>
<motion.li variants={menuItemVariants}>Product 3</motion.li>
</motion.ul>
</motion.li>
<motion.li variants={menuItemVariants}>
About
</motion.li>
</motion.ul>
</motion.nav>
);
}动态Variants
函数式Variants
tsx
const dynamicVariants = {
hidden: { opacity: 0 },
visible: (i: number) => ({
opacity: 1,
transition: {
delay: i * 0.1,
duration: 0.5
}
})
};
function DynamicVariants() {
const items = [1, 2, 3, 4, 5];
return (
<div>
{items.map((item, index) => (
<motion.div
key={item}
custom={index}
variants={dynamicVariants}
initial="hidden"
animate="visible"
>
Item {item}
</motion.div>
))}
</div>
);
}条件Variants
tsx
const conditionalVariants = {
initial: { opacity: 0, x: -100 },
animate: (direction: 'left' | 'right') => ({
opacity: 1,
x: 0,
transition: {
type: "spring",
stiffness: direction === 'right' ? 200 : 100
}
})
};
function ConditionalAnimation() {
const [direction, setDirection] = useState<'left' | 'right'>('left');
return (
<div>
<motion.div
custom={direction}
variants={conditionalVariants}
initial="initial"
animate="animate"
>
Animated Element
</motion.div>
<button onClick={() => setDirection('left')}>Left</button>
<button onClick={() => setDirection('right')}>Right</button>
</div>
);
}响应式Variants
tsx
function ResponsiveVariants() {
const isMobile = useMediaQuery('(max-width: 768px)');
const cardVariants = {
hidden: {
opacity: 0,
y: isMobile ? 20 : 50
},
visible: {
opacity: 1,
y: 0,
transition: {
type: "spring",
stiffness: isMobile ? 200 : 100
}
}
};
return (
<motion.div
variants={cardVariants}
initial="hidden"
animate="visible"
>
Responsive Card
</motion.div>
);
}高级Variants模式
序列动画
tsx
const sequenceVariants = {
initial: {
opacity: 0,
scale: 0
},
animate: {
opacity: 1,
scale: 1,
transition: {
opacity: { duration: 0.3 },
scale: {
duration: 0.5,
delay: 0.3,
type: "spring",
stiffness: 200
}
}
}
};
function SequenceAnimation() {
return (
<motion.div
variants={sequenceVariants}
initial="initial"
animate="animate"
>
Sequential Animation
</motion.div>
);
}编排复杂动画
tsx
const complexVariants = {
hidden: {
opacity: 0,
pathLength: 0,
fill: "rgba(255, 255, 255, 0)"
},
visible: {
opacity: 1,
pathLength: 1,
fill: "rgba(255, 255, 255, 1)",
transition: {
default: { duration: 2, ease: "easeInOut" },
fill: { duration: 2, ease: [1, 0, 0.8, 1] }
}
}
};
function ComplexSVGAnimation() {
return (
<motion.svg
width="200"
height="200"
viewBox="0 0 200 200"
initial="hidden"
animate="visible"
>
<motion.circle
cx="100"
cy="100"
r="80"
stroke="#00cc88"
strokeWidth="3"
variants={complexVariants}
/>
</motion.svg>
);
}状态机集成
tsx
type State = 'idle' | 'loading' | 'success' | 'error';
const stateVariants: Record<State, any> = {
idle: {
scale: 1,
opacity: 1,
},
loading: {
scale: [1, 1.1, 1],
opacity: [1, 0.5, 1],
transition: {
repeat: Infinity,
duration: 1
}
},
success: {
scale: 1,
opacity: 1,
backgroundColor: "#10b981"
},
error: {
scale: [1, 1.1, 1, 1.1, 1],
backgroundColor: "#ef4444",
transition: {
scale: {
repeat: 2,
duration: 0.2
}
}
}
};
function StateMachineAnimation() {
const [state, setState] = useState<State>('idle');
const handleAction = async () => {
setState('loading');
try {
await fetch('/api/data');
setState('success');
} catch {
setState('error');
}
setTimeout(() => setState('idle'), 2000);
};
return (
<motion.button
variants={stateVariants}
animate={state}
onClick={handleAction}
>
{state === 'idle' && 'Click Me'}
{state === 'loading' && 'Loading...'}
{state === 'success' && 'Success!'}
{state === 'error' && 'Error!'}
</motion.button>
);
}实战案例
1. 卡片网格动画
tsx
const gridContainerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
delayChildren: 0.2,
staggerChildren: 0.1
}
}
};
const gridItemVariants = {
hidden: {
y: 20,
opacity: 0,
scale: 0.8
},
visible: {
y: 0,
opacity: 1,
scale: 1,
transition: {
type: "spring",
stiffness: 100
}
},
hover: {
scale: 1.05,
transition: { duration: 0.2 }
}
};
function CardGrid() {
const cards = Array.from({ length: 12 }, (_, i) => ({
id: i,
title: `Card ${i + 1}`,
content: `Content for card ${i + 1}`
}));
return (
<motion.div
className="grid"
variants={gridContainerVariants}
initial="hidden"
animate="visible"
>
{cards.map((card) => (
<motion.div
key={card.id}
className="card"
variants={gridItemVariants}
whileHover="hover"
>
<h3>{card.title}</h3>
<p>{card.content}</p>
</motion.div>
))}
</motion.div>
);
}2. 展开式菜单
tsx
const sidebarVariants = {
open: {
width: 250,
transition: {
type: "spring",
stiffness: 400,
damping: 40
}
},
closed: {
width: 80,
transition: {
delay: 0.5,
type: "spring",
stiffness: 400,
damping: 40
}
}
};
const menuItemVariants = {
open: {
opacity: 1,
x: 0,
transition: { staggerChildren: 0.07, delayChildren: 0.2 }
},
closed: {
opacity: 0,
x: -20,
transition: { staggerChildren: 0.05, staggerDirection: -1 }
}
};
const iconVariants = {
open: { rotate: 0 },
closed: { rotate: 180 }
};
function ExpandableSidebar() {
const [isOpen, setIsOpen] = useState(true);
return (
<motion.aside
variants={sidebarVariants}
animate={isOpen ? "open" : "closed"}
className="sidebar"
>
<button onClick={() => setIsOpen(!isOpen)}>
<motion.span variants={iconVariants}>→</motion.span>
</button>
<motion.ul variants={menuItemVariants}>
{['Home', 'Profile', 'Settings', 'Logout'].map((item) => (
<motion.li key={item} variants={menuItemVariants}>
<span className="icon">📌</span>
{isOpen && <span className="text">{item}</span>}
</motion.li>
))}
</motion.ul>
</motion.aside>
);
}3. 时间轴动画
tsx
const timelineVariants = {
hidden: {
opacity: 0,
},
visible: {
opacity: 1,
transition: {
when: "beforeChildren",
staggerChildren: 0.3,
}
}
};
const itemLeftVariants = {
hidden: {
opacity: 0,
x: -50
},
visible: {
opacity: 1,
x: 0,
transition: {
type: "spring",
stiffness: 100
}
}
};
const itemRightVariants = {
hidden: {
opacity: 0,
x: 50
},
visible: {
opacity: 1,
x: 0,
transition: {
type: "spring",
stiffness: 100
}
}
};
const dotVariants = {
hidden: { scale: 0 },
visible: {
scale: 1,
transition: {
type: "spring",
stiffness: 300,
damping: 20
}
}
};
function Timeline() {
const events = [
{ id: 1, date: '2024-01', title: 'Event 1', side: 'left' },
{ id: 2, date: '2024-02', title: 'Event 2', side: 'right' },
{ id: 3, date: '2024-03', title: 'Event 3', side: 'left' },
{ id: 4, date: '2024-04', title: 'Event 4', side: 'right' },
];
return (
<motion.div
className="timeline"
variants={timelineVariants}
initial="hidden"
animate="visible"
>
{events.map((event) => (
<div key={event.id} className={`timeline-item ${event.side}`}>
<motion.div
className="timeline-content"
variants={event.side === 'left' ? itemLeftVariants : itemRightVariants}
>
<span className="date">{event.date}</span>
<h3>{event.title}</h3>
</motion.div>
<motion.div
className="timeline-dot"
variants={dotVariants}
/>
</div>
))}
</motion.div>
);
}4. 表单验证动画
tsx
const formVariants = {
initial: { opacity: 0, y: 20 },
animate: {
opacity: 1,
y: 0,
transition: {
staggerChildren: 0.1
}
}
};
const fieldVariants = {
initial: { opacity: 0, x: -20 },
animate: { opacity: 1, x: 0 },
error: {
x: [0, -10, 10, -10, 10, 0],
transition: { duration: 0.4 }
},
success: {
borderColor: "#10b981",
transition: { duration: 0.3 }
}
};
const buttonVariants = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
hover: { scale: 1.05 },
tap: { scale: 0.95 },
loading: {
opacity: 0.7,
transition: { duration: 0.2 }
}
};
function AnimatedForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [emailState, setEmailState] = useState<'initial' | 'error' | 'success'>('initial');
const [isSubmitting, setIsSubmitting] = useState(false);
const validateEmail = (value: string) => {
if (!value) {
setEmailState('initial');
} else if (!/\S+@\S+\.\S+/.test(value)) {
setEmailState('error');
} else {
setEmailState('success');
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
setIsSubmitting(false);
};
return (
<motion.form
variants={formVariants}
initial="initial"
animate="animate"
onSubmit={handleSubmit}
>
<motion.div
className="form-field"
variants={fieldVariants}
animate={emailState}
>
<label>Email</label>
<input
type="email"
value={email}
onChange={(e) => {
setEmail(e.target.value);
validateEmail(e.target.value);
}}
/>
{emailState === 'error' && (
<motion.span
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="error"
>
Invalid email
</motion.span>
)}
</motion.div>
<motion.div className="form-field" variants={fieldVariants}>
<label>Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</motion.div>
<motion.button
type="submit"
variants={buttonVariants}
whileHover={!isSubmitting ? "hover" : undefined}
whileTap={!isSubmitting ? "tap" : undefined}
animate={isSubmitting ? "loading" : "animate"}
disabled={isSubmitting}
>
{isSubmitting ? 'Submitting...' : 'Submit'}
</motion.button>
</motion.form>
);
}5. 数字滚动计数器
tsx
const counterVariants = {
hidden: { opacity: 0, y: 20 },
visible: (i: number) => ({
opacity: 1,
y: 0,
transition: {
delay: i * 0.05,
type: "spring",
stiffness: 100
}
})
};
function AnimatedCounter({ value }: { value: number }) {
const digits = value.toString().split('');
return (
<div className="counter">
{digits.map((digit, index) => (
<motion.span
key={index}
custom={index}
variants={counterVariants}
initial="hidden"
animate="visible"
className="digit"
>
{digit}
</motion.span>
))}
</div>
);
}
function CounterDemo() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(c => (c + 1) % 100000);
}, 2000);
return () => clearInterval(timer);
}, []);
return <AnimatedCounter value={count} />;
}Variants最佳实践
命名规范
tsx
// ✅ 清晰的状态命名
const cardVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1 },
hover: { scale: 1.05 },
tap: { scale: 0.95 },
exit: { opacity: 0 }
};
// ❌ 模糊的命名
const variants = {
state1: { opacity: 0 },
state2: { opacity: 1 },
state3: { scale: 1.05 }
};复用Variants
tsx
// 创建可复用的Variants
const fadeInUp = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
type: "spring",
stiffness: 100
}
}
};
const scaleIn = {
hidden: { scale: 0, opacity: 0 },
visible: {
scale: 1,
opacity: 1,
transition: {
type: "spring",
stiffness: 200
}
}
};
// 组合使用
function Component() {
return (
<>
<motion.div variants={fadeInUp} initial="hidden" animate="visible">
Content 1
</motion.div>
<motion.div variants={scaleIn} initial="hidden" animate="visible">
Content 2
</motion.div>
</>
);
}性能优化
tsx
// ✅ 缓存Variants对象
const cardVariants = useMemo(() => ({
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
}), []);
// ✅ 使用transform属性
const optimizedVariants = {
hidden: { opacity: 0, scale: 0.8 },
visible: { opacity: 1, scale: 1 }
};
// ❌ 避免使用布局属性
const unoptimizedVariants = {
hidden: { opacity: 0, width: 0 },
visible: { opacity: 1, width: 300 }
};总结
Variants核心优势
✅ 代码组织清晰
✅ 易于维护和复用
✅ 支持父子协调动画
✅ 强大的交错动画能力
✅ 支持动态和条件逻辑
✅ 便于状态管理集成使用场景
✅ 列表和网格动画
✅ 菜单和导航动画
✅ 表单交互动画
✅ 页面切换动画
✅ 加载和状态动画
✅ 复杂的编排动画性能考虑
✅ 缓存Variants对象
✅ 使用transform和opacity
✅ 合理使用staggerChildren
✅ 避免过度嵌套
✅ 测试不同设备性能Variants是Framer Motion的精髓,掌握它可以让你的动画代码更加优雅和强大。通过合理使用Variants,你可以创建复杂而协调的动画效果,显著提升应用的交互体验。