motion-ui
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMotion System v4.2
Motion System v4.2
Production-ready UI motion system for React / Next.js.
Focused on performance, accessibility, and usability — not decoration.
适用于React / Next.js的可投入生产的UI动效系统。
专注于性能、可访问性与可用性——而非装饰性效果。
When to Use
适用场景
Use this motion system when motion:
- Guides attention (e.g., onboarding, key actions)
- Communicates state (loading, success, error, transitions)
- Preserves spatial continuity (layout changes, navigation)
在以下动效场景中使用本系统:
- 引导注意力(如:新手引导、关键操作提示)
- 传达状态(加载中、成功、错误、过渡状态)
- 保持空间连续性(布局变更、导航切换)
Appropriate Scenarios
合适的场景
- Interactive components (buttons, modals, menus)
- State transitions (loading → loaded, open → closed)
- Navigation and layout continuity (shared elements, crossfade)
- 交互式组件(按钮、模态框、菜单)
- 状态过渡(加载中→加载完成、展开→收起)
- 导航与布局连续性(共享元素、淡入淡出切换)
Considerations
注意事项
- Accessibility: Always support reduced motion
- Device adaptation: Adjust for low-end devices
- Performance trade-offs: Prefer responsiveness over visual smoothness
- 可访问性:始终支持减少动效的设置
- 设备适配:针对低端设备调整动效
- 性能权衡:优先保证响应性而非视觉流畅度
Avoid Using Motion When
避免使用动效的场景
- It is purely decorative
- It reduces usability or clarity
- It impacts performance negatively
- 仅作为装饰用途
- 降低可用性或清晰度
- 对性能产生负面影响
How It Works
工作原理
Core Principle
核心原则
Motion must:
- Guide attention
- Communicate state
- Preserve spatial continuity
If it does none → remove it.
动效必须满足以下至少一项:
- 引导注意力
- 传达状态
- 保持空间连续性
若均不满足→移除该动效。
Installation
安装
bash
npm install motionbash
npm install motionVersion
版本说明
- - default for current Motion for React projects (package:
motion/react)motion - - legacy import path for projects that still depend on Framer Motion
framer-motion
Do not mix. Mixing causes conflicting internal schedulers and broken contexts — components from one package will not coordinate exit animations with components from the other.
AnimatePresenceTo check which version your project uses:
bash
cat package.json | grep -E '"motion"|"framer-motion"'Always import from one source consistently:
ts
// Correct (modern)
import { motion, AnimatePresence } from "motion/react"
// Correct (legacy)
import { motion, AnimatePresence } from "framer-motion"
// Never mix both in the same project- - 当前React项目的默认版本(包名:
motion/react)motion - - 仍依赖Framer Motion的项目使用的旧版导入路径
framer-motion
请勿混合使用。混合使用会导致内部调度器冲突,以及上下文失效——来自一个包的组件无法与另一个包的组件协调退出动画。
AnimatePresence检查项目使用的版本:
bash
cat package.json | grep -E '"motion"|"framer-motion"'请始终保持从同一源导入:
ts
// 正确(现代版)
import { motion, AnimatePresence } from "motion/react"
// 正确(旧版)
import { motion, AnimatePresence } from "framer-motion"
// 绝不要在同一项目中混合使用两者Motion Tokens
动效令牌
ts
// motionTokens.ts
export const motionTokens = {
duration: {
fast: 0.18,
normal: 0.35,
slow: 0.6
},
// Use these as the `ease` value inside a `transition` object:
// transition={{ duration: motionTokens.duration.normal, ease: motionTokens.easing.smooth }}
easing: {
smooth: [0.22, 1, 0.36, 1] as [number, number, number, number],
sharp: [0.4, 0, 0.2, 1] as [number, number, number, number]
},
distance: {
sm: 8,
md: 16,
lg: 24
}
}Usage example:
tsx
import { motionTokens } from "@/lib/motionTokens"
<motion.div
initial={{ opacity: 0, y: motionTokens.distance.md }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: motionTokens.duration.normal,
ease: motionTokens.easing.smooth
}}
/>ts
// motionTokens.ts
export const motionTokens = {
duration: {
fast: 0.18,
normal: 0.35,
slow: 0.6
},
// 在`transition`对象中使用这些值作为`ease`参数:
// transition={{ duration: motionTokens.duration.normal, ease: motionTokens.easing.smooth }}
easing: {
smooth: [0.22, 1, 0.36, 1] as [number, number, number, number],
sharp: [0.4, 0, 0.2, 1] as [number, number, number, number]
},
distance: {
sm: 8,
md: 16,
lg: 24
}
}使用示例:
tsx
import { motionTokens } from "@/lib/motionTokens"
<motion.div
initial={{ opacity: 0, y: motionTokens.distance.md }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: motionTokens.duration.normal,
ease: motionTokens.easing.smooth
}}
/>Performance Rules
性能规则
Safe
- transform
- opacity
Avoid
- width / height
- top / left
Rule: responsiveness > smoothness
安全属性
- transform
- opacity
应避免的属性
- width / height
- top / left
规则:响应性 > 流畅度
Device Adaptation
设备适配
The heuristic combines CPU core count and available memory for a more reliable signal. is available on Chrome/Android; the fallback covers Safari and Firefox.
deviceMemoryts
const isLowEnd =
typeof navigator !== "undefined" && (
// Low memory (Chrome/Android only; undefined elsewhere → treat as capable)
(navigator.deviceMemory !== undefined && navigator.deviceMemory <= 2) ||
// Few cores AND no memory API (covers Safari/Firefox on weak hardware)
(navigator.deviceMemory === undefined && navigator.hardwareConcurrency <= 4)
)
const duration = isLowEnd ? 0.2 : 0.4该判断逻辑结合了CPU核心数和可用内存,以获得更可靠的信号。在Chrome/Android中可用;回退逻辑覆盖Safari和Firefox。
deviceMemoryts
const isLowEnd =
typeof navigator !== "undefined" && (
// 低内存(仅Chrome/Android支持;其他环境下为undefined→视为高性能设备)
(navigator.deviceMemory !== undefined && navigator.deviceMemory <= 2) ||
// 核心数少且无内存API(覆盖弱硬件上的Safari/Firefox)
(navigator.deviceMemory === undefined && navigator.hardwareConcurrency <= 4)
)
const duration = isLowEnd ? 0.2 : 0.4Accessibility
可访问性
JS (useReducedMotion)
JS(useReducedMotion)
tsx
import { motion, useReducedMotion } from "motion/react"
export function FadeIn() {
const reduce = useReducedMotion()
return (
<motion.div
initial={{ opacity: 0, y: reduce ? 0 : 24 }}
animate={{ opacity: 1, y: 0 }}
/>
)
}tsx
import { motion, useReducedMotion } from "motion/react"
export function FadeIn() {
const reduce = useReducedMotion()
return (
<motion.div
initial={{ opacity: 0, y: reduce ? 0 : 24 }}
animate={{ opacity: 1, y: 0 }}
/>
)
}CSS
CSS
css
@media (prefers-reduced-motion: reduce) {
.motion-safe-transition {
transition: opacity 0.2s;
}
.motion-reduce-transform {
transform: none !important;
}
}css
@media (prefers-reduced-motion: reduce) {
.motion-safe-transition {
transition: opacity 0.2s;
}
.motion-reduce-transform {
transform: none !important;
}
}Tailwind
Tailwind
html
<div class="motion-safe:animate-fade motion-reduce:opacity-100"></div>html
<div class="motion-safe:animate-fade motion-reduce:opacity-100"></div>Architecture & Patterns
架构与模式
Core Patterns
核心模式
| Scenario | Pattern |
|---|---|
| Hover feedback | |
| Tap / press feedback | |
| Reveal on scroll | |
| Scroll-linked value | |
| Conditional mount/unmount | |
| Small layout shifts (single element, < ~300px change) | |
| Large layout shifts or full-page reflows | Avoid |
| Complex, imperative sequences | |
Why avoidon large containers? Framer's layout animation useslayoutto reconcile positions, but on elements that span the full viewport or trigger deep reflow, the measurement cost causes visible jank and CLS. Prefer CSS Grid/Flexbox transitions or coordinate withtransformon specific child elements only.layoutId
| 场景 | 模式 |
|---|---|
| 悬停反馈 | |
| 点击/按压反馈 | |
| 滚动时显示 | |
| 滚动关联值 | |
| 条件挂载/卸载 | |
| 小范围布局偏移(单个元素,变化<~300px) | |
| 大范围布局偏移或整页重排 | 避免使用 |
| 复杂的命令式序列 | |
为何避免在大型容器上使用? Framer的布局动画使用layout来协调位置,但在跨整个视口或触发深层重排的元素上,测量成本会导致可见的卡顿和CLS(累积布局偏移)。优先使用CSS Grid/Flexbox过渡,或仅在特定子元素上配合transform使用。layoutId
Layout & Transitions
布局与过渡
- Shared element transitions → (must be unique per mounted instance)
layoutId - Enter / exit transitions → (see
AnimatePresenceguidance below)mode
- 共享元素过渡 → (每个挂载实例必须唯一)
layoutId - 进入/退出过渡 → (参见下方
AnimatePresence指南)mode
AnimatePresence mode
modeAnimatePresence mode
modeAlways specify explicitly — the default () runs enter and exit simultaneously, which causes visual overlap in most UI patterns.
mode"sync" | When to use |
|---|---|
| Exit completes before enter starts. Use for modals, toasts, page transitions. |
| Enter and exit overlap. Use only when overlap is intentional (e.g., crossfade carousels). |
| Exiting element is popped out of flow immediately; remaining items animate to fill. Use for lists, tabs, dismissible cards. |
tsx
// Modal — always use "wait"
<AnimatePresence mode="wait">
{open && <Modal key="modal" />}
</AnimatePresence>
// Dismissible list item — use "popLayout"
<AnimatePresence mode="popLayout">
{items.map(item => <Card key={item.id} />)}
</AnimatePresence>请始终显式指定——默认值()会同时运行进入和退出动画,这在大多数UI模式中会导致视觉重叠。
mode"sync" | 使用场景 |
|---|---|
| 退出动画完成后再开始进入动画。用于模态框、提示框、页面过渡。 |
| 进入和退出动画重叠。仅在需要故意重叠时使用(如:淡入淡出轮播)。 |
| 退出元素立即从流中移除;剩余元素动画填充空位。用于列表、标签页、可关闭卡片。 |
tsx
// 模态框——始终使用"wait"
<AnimatePresence mode="wait">
{open && <Modal key="modal" />}
</AnimatePresence>
// 可关闭列表项——使用"popLayout"
<AnimatePresence mode="popLayout">
{items.map(item => <Card key={item.id} />)}
</AnimatePresence>Advanced Patterns (Concepts)
进阶模式(概念)
- Parallax (scroll-linked transforms)
- Scroll storytelling (sticky sections)
- 3D tilt (pointer-based transforms)
- Crossfade (shared )
layoutId - Progressive reveal (clip-path)
- Skeleton loading (looped opacity)
- Micro-interactions (hover/tap feedback)
- Spring system (physics-based motion)
- 视差效果(滚动关联变换)
- 滚动叙事(粘性区块)
- 3D倾斜(基于指针的变换)
- 淡入淡出切换(共享)
layoutId - 渐进式显示(clip-path)
- 骨架屏加载(循环透明度动画)
- 微交互(悬停/点击反馈)
- 弹簧系统(基于物理的动效)
Modal Essentials
模态框要点
- Focus trap
- Escape close
- Scroll lock
- ARIA roles
- Use so exit animation completes before the next modal enters
AnimatePresence mode="wait"
- 焦点陷阱
- 按ESC关闭
- 滚动锁定
- ARIA角色
- 使用,确保退出动画完成后再进入下一个模态框
AnimatePresence mode="wait"
Full Example
完整示例
tsx
import React, { useEffect, useRef, useState } from "react"
import { motion, AnimatePresence } from "motion/react"
function useFocusTrap(ref: React.RefObject<HTMLDivElement | null>, active: boolean) {
useEffect(() => {
if (!active || !ref.current) return
const el = ref.current
const focusable = el.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
const first = focusable[0]
const last = focusable[focusable.length - 1]
function handleKey(e: KeyboardEvent) {
if (e.key !== "Tab") return
if (e.shiftKey && document.activeElement === first) {
e.preventDefault()
last?.focus()
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault()
first?.focus()
}
}
el.addEventListener("keydown", handleKey)
first?.focus()
return () => el.removeEventListener("keydown", handleKey)
}, [active, ref])
}
function useScrollLock(active: boolean) {
useEffect(() => {
if (!active) return
const prev = document.body.style.overflow
document.body.style.overflow = "hidden"
return () => { document.body.style.overflow = prev }
}, [active])
}
function Modal({ open, closeModal }: { open: boolean; closeModal: () => void }) {
const ref = useRef<HTMLDivElement>(null)
useFocusTrap(ref, open)
useScrollLock(open)
useEffect(() => {
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") closeModal()
}
if (open) window.addEventListener("keydown", onKey)
return () => window.removeEventListener("keydown", onKey)
}, [open, closeModal])
return (
// mode="wait" ensures exit animation finishes before any new modal enters
<AnimatePresence mode="wait">
{open && (
<motion.div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 flex items-center justify-center bg-black/40"
>
<motion.div
ref={ref}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
transition={{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }}
className="bg-white p-6 rounded"
>
<h2 id="modal-title">Dialog Title</h2>
<button onClick={closeModal}>Close</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
)
}
export function Example() {
const [open, setOpen] = useState(false)
return (
<>
<button onClick={() => setOpen(true)}>Open</button>
<Modal open={open} closeModal={() => setOpen(false)} />
</>
)
}tsx
import React, { useEffect, useRef, useState } from "react"
import { motion, AnimatePresence } from "motion/react"
function useFocusTrap(ref: React.RefObject<HTMLDivElement | null>, active: boolean) {
useEffect(() => {
if (!active || !ref.current) return
const el = ref.current
const focusable = el.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
const first = focusable[0]
const last = focusable[focusable.length - 1]
function handleKey(e: KeyboardEvent) {
if (e.key !== "Tab") return
if (e.shiftKey && document.activeElement === first) {
e.preventDefault()
last?.focus()
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault()
first?.focus()
}
}
el.addEventListener("keydown", handleKey)
first?.focus()
return () => el.removeEventListener("keydown", handleKey)
}, [active, ref])
}
function useScrollLock(active: boolean) {
useEffect(() => {
if (!active) return
const prev = document.body.style.overflow
document.body.style.overflow = "hidden"
return () => { document.body.style.overflow = prev }
}, [active])
}
function Modal({ open, closeModal }: { open: boolean; closeModal: () => void }) {
const ref = useRef<HTMLDivElement>(null)
useFocusTrap(ref, open)
useScrollLock(open)
useEffect(() => {
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") closeModal()
}
if (open) window.addEventListener("keydown", onKey)
return () => window.removeEventListener("keydown", onKey)
}, [open, closeModal])
return (
// mode="wait"确保退出动画完成后再进入新的模态框
<AnimatePresence mode="wait">
{open && (
<motion.div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 flex items-center justify-center bg-black/40"
>
<motion.div
ref={ref}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
transition={{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }}
className="bg-white p-6 rounded"
>
<h2 id="modal-title">对话框标题</h2>
<button onClick={closeModal}>关闭</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
)
}
export function Example() {
const [open, setOpen] = useState(false)
return (
<>
<button onClick={() => setOpen(true)}>打开</button>
<Modal open={open} closeModal={() => setOpen(false)} />
</>
)
}SSR Safety
SSR安全
- Match initial states between server and client renders
- Avoid implicit animation origins (always set explicitly)
initial - Wrap motion components in in Next.js App Router
"use client"
- 保持服务端与客户端渲染的初始状态一致
- 避免隐式动画起点(始终显式设置)
initial - 在Next.js App Router中,将动效组件包裹在中
"use client"
Debugging
调试
Check:
- Wrong import (mixing and
motion/react)framer-motion - Missing directive in Next.js App Router
"use client" - Missing prop on
keychildrenAnimatePresence - Hydration mismatch (initial state differs between SSR and client)
- prop misuse on large containers causing reflow jank
layout - State-driven animation not triggering (check dependency arrays)
检查以下内容:
- 错误的导入(混合使用和
motion/react)framer-motion - Next.js App Router中缺少指令
"use client" - 子元素缺少
AnimatePresence属性key - 水化不匹配(SSR与客户端初始状态不同)
- 在大型容器上错误使用属性导致重排卡顿
layout - 状态驱动的动画未触发(检查依赖数组)
QA
QA检查项
- No CLS
- Keyboard works
- Focus trapped in modals
- ARIA roles correct (,
role="dialog")aria-modal="true" - Reduced motion respected (+ CSS media query)
useReducedMotion - No hydration warnings in Next.js
- Animations stop cleanly on unmount (no memory leaks)
- set explicitly on all usage sites
AnimatePresence mode
- 无CLS(累积布局偏移)
- 键盘操作正常
- 模态框中焦点被正确捕获
- ARIA角色正确(,
role="dialog")aria-modal="true" - 尊重减少动效的偏好(+ CSS媒体查询)
useReducedMotion - Next.js中无水化警告
- 卸载时动画干净停止(无内存泄漏)
- 所有使用场景均显式设置
AnimatePresencemode
Anti-Patterns
反模式
- Animating layout properties (,
width,height,top)left - Infinite animations without purpose (always ask: what state does this communicate?)
- Over-staggering lists (keep ≤ 0.1s; beyond that it feels slow)
staggerChildren - Ignoring reduced motion preferences
- Using on large or full-viewport containers
layout - Omitting on
mode(defaultAnimatePresencecauses visual overlap)"sync" - Using motion purely for decoration
- 对布局属性设置动画(,
width,height,top)left - 无意义的无限动画(始终问自己:这传达了什么状态?)
- 列表动画过度错开(≤ 0.1s;超过这个值会感觉缓慢)
staggerChildren - 忽略减少动效的偏好
- 在大型或全屏容器上使用
layout - 省略
AnimatePresence(默认mode会导致视觉重叠)"sync" - 动效仅用于装饰
Philosophy
设计理念
Motion is interaction design.
动效是交互设计的一部分。
Final Rule
最终规则
If motion does not improve UX → remove it.
如果动效没有提升用户体验→移除它。
Examples
示例
Button Interaction
按钮交互
tsx
import { motion } from "motion/react"
export function Button() {
return (
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.97 }}
transition={{ duration: 0.15, ease: [0.4, 0, 0.2, 1] }}
>
Click me
</motion.button>
)
}tsx
import { motion } from "motion/react"
export function Button() {
return (
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.97 }}
transition={{ duration: 0.15, ease: [0.4, 0, 0.2, 1] }}
>
点击我
</motion.button>
)
}Reduced Motion Example
减少动效示例
tsx
import { motion, useReducedMotion } from "motion/react"
export function FadeIn() {
const reduce = useReducedMotion()
return (
<motion.div
initial={{ opacity: 0, y: reduce ? 0 : 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: reduce ? 0.1 : 0.35, ease: [0.22, 1, 0.36, 1] }}
/>
)
}tsx
import { motion, useReducedMotion } from "motion/react"
export function FadeIn() {
const reduce = useReducedMotion()
return (
<motion.div
initial={{ opacity: 0, y: reduce ? 0 : 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: reduce ? 0.1 : 0.35, ease: [0.22, 1, 0.36, 1] }}
/>
)
}Stagger List
错开列表动画
tsx
import { motion } from "motion/react"
const container = {
hidden: {},
visible: {
transition: { staggerChildren: 0.08 } // keep ≤ 0.1s to avoid sluggishness
}
}
const item = {
hidden: { opacity: 0, y: 10 },
visible: { opacity: 1, y: 0, transition: { duration: 0.3, ease: [0.22, 1, 0.36, 1] } }
}
export function List() {
return (
<motion.ul variants={container} initial="hidden" animate="visible">
{[1, 2, 3].map(i => (
<motion.li key={i} variants={item}>Item {i}</motion.li>
))}
</motion.ul>
)
}tsx
import { motion } from "motion/react"
const container = {
hidden: {},
visible: {
transition: { staggerChildren: 0.08 } // 保持≤0.1s以避免卡顿感
}
}
const item = {
hidden: { opacity: 0, y: 10 },
visible: { opacity: 1, y: 0, transition: { duration: 0.3, ease: [0.22, 1, 0.36, 1] } }
}
export function List() {
return (
<motion.ul variants={container} initial="hidden" animate="visible">
{[1, 2, 3].map(i => (
<motion.li key={i} variants={item}>项目 {i}</motion.li>
))}
</motion.ul>
)
}Modal with AnimatePresence
带AnimatePresence的模态框
tsx
import { motion, AnimatePresence } from "motion/react"
export function Modal({ open }: { open: boolean }) {
return (
<AnimatePresence mode="wait">
{open && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }}
/>
)}
</AnimatePresence>
)
}tsx
import { motion, AnimatePresence } from "motion/react"
export function Modal({ open }: { open: boolean }) {
return (
<AnimatePresence mode="wait">
{open && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }}
/>
)}
</AnimatePresence>
)
}Scroll Parallax
滚动视差
tsx
import { useScroll, useTransform, motion } from "motion/react"
export function Parallax() {
const { scrollYProgress } = useScroll()
const y = useTransform(scrollYProgress, [0, 1], [0, -80])
return <motion.div style={{ y }} />
}tsx
import { useScroll, useTransform, motion } from "motion/react"
export function Parallax() {
const { scrollYProgress } = useScroll()
const y = useTransform(scrollYProgress, [0, 1], [0, -80])
return <motion.div style={{ y }} />
}Skeleton Loading
骨架屏加载
tsx
import { motion } from "motion/react"
export function Skeleton() {
return (
<motion.div
className="bg-gray-200 h-6 w-full rounded"
animate={{ opacity: [0.5, 1, 0.5] }}
transition={{
duration: 1.5, // comfortable pulse — was missing, caused fast flash
repeat: Infinity,
ease: "easeInOut"
}}
/>
)
}tsx
import { motion } from "motion/react"
export function Skeleton() {
return (
<motion.div
className="bg-gray-200 h-6 w-full rounded"
animate={{ opacity: [0.5, 1, 0.5] }}
transition={{
duration: 1.5, // 舒适的脉冲效果——之前缺失导致快速闪烁
repeat: Infinity,
ease: "easeInOut"
}}
/>
)
}Shared Layout (Crossfade)
共享布局(淡入淡出切换)
tsx
import { motion } from "motion/react"
// layoutId must be unique per mounted instance.
// If multiple instances can exist simultaneously, append a unique id:
// layoutId={`shared-${item.id}`}
export function Shared() {
return <motion.div layoutId="shared" />
}tsx
import { motion } from "motion/react"
// layoutId必须每个挂载实例唯一。
// 如果可能同时存在多个实例,请追加唯一id:
// layoutId={`shared-${item.id}`}
export function Shared() {
return <motion.div layoutId="shared" />
}