UI交互动画演示项目实战:React+Vite+Framer Motion+GSAP 这次我们直接看一个实际问题为什么同样的设计方案有人演示出来像是高级产品发布会有人演示出来就是干巴巴的静态稿截图核心差别往往就在 UI 交互动画。UI 交互动画不是“加个过渡效果”那么简单。它是产品演示里的叙事手段页面怎么进场、卡片怎么反馈、列表怎么重排、滚动时内容如何渐进呈现这些细节决定了方案评审时对方是“看懂了”还是“被说服了”。对前端工程师来说一套可复用、可配置、性能可控的交互动画方案也能把还原度从“大概差不多”提升到“几乎逐帧一致”。这篇文章不聊虚的直接给一套可落地的方案用 React Vite Framer Motion GSAP 搭一个 UI 交互动画演示项目从项目初始化、组件动画封装、滚动驱动叙事、配置化批量场景到性能观察和常见问题排查全部走一遍。读者可以照着跑通然后把这套思路搬到自己的产品演示、组件库或官网落地页里。如果你正在做 UI/UX 设计、前端组件开发、产品方案演示或者想给自己的开源组件库加一套高质量的动效样例这篇文章可以收藏备用。1. UI 交互动画核心能力速览先给一张速览表快速判断这套方案适不适合你。能力项说明项目类型UI 交互动画演示工程 / 设计还原与 Prototype 实现技术栈React 18 Vite TypeScript Framer Motion GSAP核心功能入场/出场动画、拖拽交互、列表重排、滚动驱动动画、路由切换动效、配置化动画预设启动方式npm 命令启动开发服务浏览器直接访问是否需要 GPU不需要独立显卡依赖浏览器 GPU 合成支持平台Windows / macOS / Linux现代浏览器即可是否支持批量场景支持通过 JSON/TS 配置驱动多个场景批量切换是否提供接口 API不涉及后端 API但动画预设可封装为前端组件接口复用上手门槛需要基础 React 和 CSS 知识Framer Motion 和 GSAP 的 API 可以边写边查适合场景产品方案演示、官网落地页、设计系统动效规范、组件库交互动画、UI 自动化测试前的视觉基线这套方案的优点是把“设计想法”变成“可运行的 Demo”并且动画参数集中管理方便设计师和前端一起调参。局限是它服务于界面表现层不处理业务数据逻辑也不适合做重型后台应用的全部交互。2. 适用场景与使用边界2.1 能解决什么问题UI 交互动画最常见的价值场景有三个。第一个是方案评审。静态稿只能说明布局和视觉但页面跳转层级、卡片拖拽反馈、列表项增删时的布局变化用动画 Demo 演示比口头解释十遍都清楚。评审会上一旦对方看到“真实的动效反馈”决策效率会明显提升。第二个是开发还原。设计师定了 240ms 的缓动曲线、8px 的位移距离、0.2 的透明度变化如果前端手里没有可调参数的工具还原时全靠肉眼猜测。把动画参数收敛成统一的配置对象后设计师改一个数值前端预览立刻同步沟通成本低很多。第三个是官网和产品展示。落地页滚动叙事、功能模块渐进呈现、卡片悬浮反馈这些动效直接影响用户对产品品质感的判断。文章后面会演示一套基于 GSAP ScrollTrigger 的滚动驱动效果适合直接裁剪到官网项目里。2.2 不适合什么场景UI 交互动画不等于“所有页面都要动”。后台管理系统里的表格批量操作、表单校验错误、高频数据刷新区域动画太多反而干扰操作效率。低端移动设备上的复杂粒子动画、全屏 3D 场景也需要谨慎评估性能和耗电。另外无障碍设计是硬边界。动画时长不能只做 100ms 的闪变要在代码里尊重系统的prefers-reduced-motion设置用户开启“减少动态效果”时自动关闭非必要动画。2.3 版权与合规边界如果项目里的演示素材、图标、插画来自第三方资源站要确认授权范围尤其是商用场景。涉及用户界面截图、他人产品界面做“对比演示”时建议模糊敏感信息和商标改成抽象的示例数据。动效本身不存在版权问题但素材必须有授权。3. 环境准备与前置条件动手之前先列一份检查清单避免中途反复补环境。检查项要求操作系统Windows 10/11、macOS 12、主流 Linux 发行版均可Node.js建议 18.x 或 20.x LTS 版本包管理器npm 或 pnpm本文示例用 npm浏览器Chrome / Edge 最新版需要支持 Web Animations API代码编辑器VS Code 或其他前端 IDEGit可选便于保存各阶段版本验证 Node.js 是否就绪命令行执行node -v npm -v如果提示命令不存在需要先安装 Node.js。4. UI 交互动画演示项目搭建与核心实现这一节从零开始搭一个可运行的 UI 交互动画演示工程。分四步初始化项目、安装依赖、编写动画组件、启动服务。4.1 初始化项目使用 Vite 创建 React TypeScript 模板npm create vitelatest ui-motion-demo -- --template react-ts cd ui-motion-demo npm install安装动画相关依赖npm install framer-motion gsap lucide-reactframer-motion负责组件级动画入场/出场、拖拽、布局动画。gsapgsap/ScrollTrigger负责滚动驱动的叙事动画。lucide-react提供演示用的图标避免手工切图。4.2 目录结构设计src/ ├── App.tsx # 路由与整体布局 ├── main.tsx # 入口文件 ├── index.css # 全局样式 ├── motion/ │ ├── presets.ts # 动画预设配置 │ └── MotionCard.tsx # 可复用动画卡片组件 ├── scenes/ │ ├── HeroScene.tsx # 首屏入场动画 │ ├── DragScene.tsx # 拖拽排序交互 │ ├── ScrollScene.tsx # 滚动驱动动画 │ └── ListScene.tsx # 列表增删与布局动画 └── data/ └── demo-data.ts # 演示数据配置这种分层的好处是动画配置和页面结构分开后面批量调整动画时长或缓动曲线时只改presets.ts一个文件。4.3 动画预设配置把常用的动画参数收敛成配置对象这是整个方案里工程价值最高的部分。设计师调整动效时不需要翻组件代码改配置即可。// src/motion/presets.ts export const motionPresets { fadeUp: { initial: { opacity: 0, y: 24 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -12 }, transition: { duration: 0.4, ease: [0.22, 1, 0.36, 1] } }, scaleIn: { initial: { opacity: 0, scale: 0.92 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.96 }, transition: { duration: 0.3, ease: easeOut } }, listItem: { initial: { opacity: 0, x: -16 }, animate: { opacity: 1, x: 0 }, exit: { opacity: 0, x: 16 }, transition: { duration: 0.25, ease: easeOut } } } as const; export type MotionPresetName keyof typeof motionPresets;这里用了cubic-bezier(0.22, 1, 0.36, 1)这是较常见的“出场缓动”曲线比默认的ease-in-out更有速度感。实际项目里可以按设计规范替换。4.4 可复用动画卡片组件演示页面里的卡片、列表项、功能块统一封装成MotionCard。组件接受预设名称内部用AnimatePresence处理进场和退场。// src/motion/MotionCard.tsx import { motion, AnimatePresence } from framer-motion; import { motionPresets, type MotionPresetName } from ./presets; interface MotionCardProps { id: string; title: string; description: string; preset?: MotionPresetName; onRemove?: (id: string) void; } export function MotionCard({ id, title, description, preset fadeUp, onRemove }: MotionCardProps) { const presetConfig motionPresets[preset]; return ( motion.div layout initial{presetConfig.initial} animate{presetConfig.animate} exit{presetConfig.exit} transition{presetConfig.transition} whileHover{{ y: -4, boxShadow: 0 12px 32px rgba(0,0,0,0.12) }} classNamemotion-card div classNamemotion-card__header h3{title}/h3 {onRemove ( button onClick{() onRemove(id)} aria-label{移除 ${title}} × /button )} /div p{description}/p /motion.div ); } export { AnimatePresence };注意layout属性它让 Framer Motion 自动处理元素位置变化的平滑过渡。列表项删除后其他卡片会“推挤”到新位置而不是瞬间跳变。这是 UI 交互动画里“高级感”的重要来源。4.5 列表增删的布局动画场景实现一个可增删卡片的列表对应后台管理、标签管理、待办事项这类常见交互。核心在于使用AnimatePresence包裹列表项并给每个项设置layout属性。// src/scenes/ListScene.tsx import { useState } from react; import { AnimatePresence, MotionCard } from ../motion/MotionCard; import type { MotionPresetName } from ../motion/presets; interface DemoItem { id: string; title: string; description: string; } const initialItems: DemoItem[] [ { id: 1, title: 项目看板, description: 卡片横向拖拽排序状态实时同步 }, { id: 2, title: 标签编辑器, description: 标签增删时自动重排不遮挡内容 }, { id: 3, title: 任务列表, description: 任务完成时带动画移入完成分组 } ]; export function ListScene() { const [items, setItems] useState(initialItems); const [preset, setPreset] useStateMotionPresetName(fadeUp); const addItem () { const id String(Date.now()); setItems((prev) [ ...prev, { id, title: 新卡片 ${id.slice(-4)}, description: 通过动画预设配置生成的演示卡片 } ]); }; const removeItem (id: string) { setItems((prev) prev.filter((item) item.id ! id)); }; return ( section classNamescene div classNamescene__toolbar button onClick{addItem}添加卡片/button select value{preset} onChange{(e) setPreset(e.target.value as MotionPresetName)} option valuefadeUp淡入上移/option option valuescaleIn缩放进场/option option valuelistItem左滑切入/option /select /div div classNamescene__list AnimatePresence modepopLayout {items.map((item) ( MotionCard key{item.id} id{item.id} title{item.title} description{item.description} preset{preset} onRemove{removeItem} / ))} /AnimatePresence /div /section ); }关键点是AnimatePresence modepopLayout它保证删除元素时剩余元素不会因为退出动画占位而跳动。添加卡片时Framer Motion 的layout会自动计算所有卡片的新位置并做平滑过渡。4.6 拖拽排序的交互场景拖拽是 UI 产品演示里很“抢眼”的交互。Framer Motion 的drag属性可以快速实现。// src/scenes/DragScene.tsx import { useState } from react; import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from dnd-kit/core; import { SortableContext, useSortable, verticalListSortingStrategy } from dnd-kit/sortable; import { CSS } from dnd-kit/utilities; const items [ { id: 1, label: 设置 }, { id: 2, label: 通知 }, { id: 3, label: 隐私 }, { id: 4, label: 账号 } ]; function SortableItem({ id, label }: { id: string; label: string }) { const { attributes, listeners, setNodeRef, transform, transition } useSortable({ id }); const style { transform: CSS.Transform.toString(transform), transition }; return ( div ref{setNodeRef} style{style} classNamedrag-item {...attributes} {...listeners} {label} /div ); } export function DragScene() { const [order, setOrder] useState(items); const sensors useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } })); return ( DndContext sensors{sensors} collisionDetection{closestCenter} onDragEnd{(event) { const { active, over } event; if (!over || active.id over.id) return; setOrder((prev) { const oldIndex prev.findIndex((item) item.id active.id); const newIndex prev.findIndex((item) item.id over.id); const next [...prev]; const [moved] next.splice(oldIndex, 1); next.splice(newIndex, 0, moved); return next; }); }} SortableContext items{order.map((item) item.id)} strategy{verticalListSortingStrategy} div classNamedrag-list {order.map((item) ( SortableItem key{item.id} id{item.id} label{item.label} / ))} /div /SortableContext /DndContext ); }拖拽部分的依赖是dnd-kit/core和dnd-kit/sortable这是当前 React 生态里比较成熟的拖拽排序方案。演示时用户按住列表项拖动其他项自动让位松手后列表重新排列整个过程有明确的物理反馈。需要安装这两个额外包npm install dnd-kit/core dnd-kit/sortable dnd-kit/utilities4.7 滚动驱动的场景叙事动画官网和产品演示页最常见的“高级感”来自滚动叙事随着页面滚动内容逐个浮现。使用 GSAP ScrollTrigger 实现。// src/scenes/ScrollScene.tsx import { useEffect, useRef } from react; import gsap from gsap; import { ScrollTrigger } from gsap/ScrollTrigger; gsap.registerPlugin(ScrollTrigger); const scrollBlocks [ { title: 01 快速定位, description: 页面滚动时模块依次进场层次清晰 }, { title: 02 参数可调, description: 位移、透明度、旋转角度全部集中配置 }, { title: 03 性能可控, description: 使用 transform 和 opacity避免触发重排 }, { title: 04 易于集成, description: 可直接拆到官网落地页使用 } ]; export function ScrollScene() { const sectionRef useRefHTMLDivElement(null); useEffect(() { const ctx gsap.context(() { gsap.utils.toArrayHTMLElement(.scroll-block).forEach((block) { gsap.from(block, { opacity: 0, y: 48, duration: 0.6, ease: power3.out, scrollTrigger: { trigger: block, start: top 80%, toggleActions: play none none reverse } }); }); }, sectionRef); return () ctx.revert(); }, []); return ( section ref{sectionRef} classNamescene scene--scroll {scrollBlocks.map((block) ( div classNamescroll-block key{block.title} h3{block.title}/h3 p{block.description}/p /div ))} /section ); }这个实现里每个.scroll-block进入视口 80% 位置时触发动画滚出视口后反向播放。gsap.context和ctx.revert()是 GSAP 3.11 的标准清理方式防止组件卸载后动画残留。4.8 首屏入场与整体布局最后在App.tsx里把这些场景组合起来并给首屏加一个整体入场动画。// src/App.tsx import { motion } from framer-motion; import { HeroScene } from ./scenes/HeroScene; import { DragScene } from ./scenes/DragScene; import { ScrollScene } from ./scenes/ScrollScene; import { ListScene } from ./scenes/ListScene; export default function App() { return ( main classNameapp motion.header initial{{ opacity: 0, y: -20 }} animate{{ opacity: 1, y: 0 }} transition{{ duration: 0.5, ease: easeOut }} classNameapp__header h1UI 交互动画演示/h1 pFramer Motion GSAP ScrollTrigger 驱动的产品演示场景/p /motion.header HeroScene / ListScene / DragScene / ScrollScene / /main ); }补充一份基础样式index.css至少包含按钮、卡片、列表的基础布局和交互状态。样式不是这篇的重点但如果没有基础布局动画演示效果会打折扣。/* src/index.css 关键片段 */ * { box-sizing: border-box; } body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; background: #f5f6f8; color: #1a1a1a; } .app { max-width: 860px; margin: 0 auto; padding: 24px 20px 120px; } .app__header { text-align: center; margin-bottom: 40px; } .scene { background: #ffffff; border-radius: 16px; padding: 24px; margin-bottom: 48px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06); } .motion-card { border: 1px solid #e5e7eb; border-radius: 12px; padding: 16px; margin-bottom: 12px; background: #fafafa; } .motion-card__header { display: flex; justify-content: space-between; align-items: center; } .motion-card button { border: none; background: transparent; font-size: 20px; cursor: pointer; color: #888; } .drag-list { display: flex; flex-direction: column; gap: 8px; } .drag-item { background: #eef2ff; border-radius: 10px; padding: 14px 16px; cursor: grab; user-select: none; touch-action: none; } .scene--scroll { min-height: 600px; } .scroll-block { min-height: 120px; margin-bottom: 24px; padding: 20px; border-radius: 12px; background: #f0fdf4; } media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }最后启动开发服务npm run dev浏览器访问http://localhost:5173即可看到演示页面。5. UI 交互动画功能测试与效果验证项目跑起来之后重点验证五个场景每个场景都有明确的判断标准。5.1 页面首屏入场刷新页面头部标题应该从上方淡入下滑约 0.5 秒内完成。成功标准标题和副标题不是瞬间出现有短暂的位移动画。失败排查如果页面直接静态显示检查motion.header是否被AnimatePresence或条件渲染影响确认 Framer Motion 版本正常加载。5.2 列表增删与布局重排点击“添加卡片”新卡片应在列表末尾以淡入上移动画进入同时已有卡片通过layout动画平滑让位。点击卡片右上角“×”卡片先执行退出动画位置让给后续卡片。成功标准删除卡片时剩余卡片是“推挤式”移动到新位置而不是瞬间跳变。失败排查如果删除时位置跳变检查卡片根节点是否有layout属性以及AnimatePresence modepopLayout是否生效。5.3 动画预设切换在 ListScene 的工具栏切换“淡入上移 / 缩放进场 / 左滑切入”新添加的卡片使用不同动画风格进入。成功标准切换预设后不刷新页面也能立即生效。失败排查确认presets.ts中导出的动画配置字段与组件内部使用的字段一致transition里的duration和ease类型是否正确。5.4 拖拽排序在 DragScene 中按住任意菜单项拖动其他项应自动避让松手后列表重排。成功标准拖拽过程跟手松手后位置正确没有抖动。失败排查如果拖拽没反应检查PointerSensor的activationConstraint是否过小导致误触或组件是否包裹在DndContext内。5.5 滚动驱动动画在 ScrollScene 区域向下滚动四个模块应依次在进入视口时淡入上移。向上滚动时模块反向退出。成功标准滚动到模块位置时动画触发滚出时模块恢复初始状态。失败排查如果模块一直显示没有动画打开控制台确认ScrollTrigger是否注册成功检查start: top 80%是否因为父容器高度不够而无法触发。6. 数据驱动的动画配置与批量场景这里不涉及后端 API但 UI 交互动画同样有“批量”需求几十个组件要使用同一套动画规范不可能逐个手写。解决方案是把动画预设集中管理并通过一份配置数据驱动所有场景。6.1 配置数据示例// src/data/demo-data.ts export interface SceneConfig { id: string; title: string; preset: fadeUp | scaleIn | listItem; description: string; } export const sceneConfigs: SceneConfig[] [ { id: scene-1, title: 模块一设计规范, preset: fadeUp, description: 使用统一动画预设保证全站动效一致 }, { id: scene-2, title: 模块二产品能力, preset: scaleIn, description: 缩放进场适合强调重点模块 }, { id: scene-3, title: 模块三团队介绍, preset: listItem, description: 列表切入适合人物卡片的依次展示 } ];实际演示项目里ListScene已经通过 demo-data 渲染卡片列表。当某个配置项改变时所有引用该数据的卡片同时更新动画这就是“配置文件驱动的 UI 交互动画批量管理”。6.2 批量处理脚本思路如果要在 CI 流程里批量检查动画配置可以写一个简单的 Node 脚本读取presets.ts或 JSON 配置校验动画时长是否在合理范围内。# 用 tsx 直接运行 TypeScript 检查脚本 npm install -D tsx// scripts/check-presets.ts import { motionPresets } from ../src/motion/presets; const MAX_DURATION 0.8; for (const [name, config] of Object.entries(motionPresets)) { const duration config.transition.duration; if (duration MAX_DURATION) { console.warn([动画配置检查] ${name} 时长 ${duration}s 超过 ${MAX_DURATION}s建议优化); } else { console.log([动画配置检查] ${name} 时长 ${duration}s通过); } }运行方式npx tsx scripts/check-presets.ts这个脚本可以做成 git hook提交前自动检查动画时长和预设命名避免设计师在某个页面里单独写死一套特殊动画导致风格失控。7. 性能与资源占用观察UI 交互动画性能观察重点是确认动画是跑在合成器线程上还是触发了主线程重排。打开 Chrome DevTools 的 Performance 面板录制一段动画过程观察帧率和主线程任务时间。7.1 优先使用 transform 和 opacityFramer Motion 默认用 GPU 合成但这只对transform和opacity有效。如果动画里改了width、height、top、margin浏览器会触发布局计算在低端设备上掉帧明显。封装组件时尽量只用y、x、scale、opacity这些属性。7.2 滚动场景的性能控制GSAP ScrollTrigger 的gsap.from在页面同时存在大量滚动动画时需要关注 start 和 end 的计算频率。对于长页面建议给每个触发块加once: true或合理设置toggleActions避免反复回滚造成重复计算。7.3 内存观察打开 DevTools 的 Memory 面板录制一次页面加载执行“添加卡片 / 删除卡片”各十次再看内存是否存在持续增长。如果每次操作后内存只升不降说明组件卸载时动画监听没有正确清理。上面代码里使用gsap.contextctx.revert()就是为规避这类问题。7.4 帧率估算即使没有专业工具也可以用一段简单的脚本估算帧率// 浏览器控制台运行 let frames 0; const start performance.now(); function count() { frames; requestAnimationFrame(count); } requestAnimationFrame(count); setTimeout(() { const elapsed (performance.now() - start) / 1000; console.log(近 ${elapsed.toFixed(1)}s 平均帧率: ${(frames / elapsed).toFixed(1)} fps); }, 3000);正常 UI 动画应稳定在 50fps 以上低于 30fps 就需要检查是否存在大量重排或 JavaScript 主线程被阻塞。8. UI 交互动画常见问题与排查方法问题现象可能原因排查方式解决方案动画不触发组件直接显示最终状态Framer Motion 组件未正确接收initial属性或 StrictMode 下状态被重置打开 React DevTools 检查组件的 props确认initial存在必要时给组件加key强制重新挂载列表删除时其他卡片瞬间跳位卡片根节点缺少layout属性或AnimatePresence模式不合适检查列表项结构确认AnimatePresence包裹了所有列表项给卡片加layout使用modepopLayout滚动动画不触发ScrollTrigger 注册失败、触发点计算错误、容器高度不足控制台执行ScrollTrigger.getAll()查看实例注册插件调整start值给容器设置最小高度拖拽列表只有第一次能拖之后失效监听事件在组件重渲染后丢失或 key 冲突检查组件的 key 是否稳定使用稳定的id作为 key避免使用数组索引动画卡顿掉帧动画属性触发重排或主线程被大量任务阻塞Performance 面板录制观察只使用transform/opacity减少同屏动画数量用户开启“减少动态效果”后动画仍然运行未监听prefers-reduced-motion检查系统设置和代码中的媒体查询在 CSS 或代码中关闭非必要动画GSAP 动画在组件卸载后仍执行没有调用ctx.revert()或清除 ScrollTrigger控制台查看 ScrollTrigger 实例数量是否持续增加在 useEffect cleanup 里调用ctx.revert()切换预设后动画风格不变配置对象引用未更新或组件未重新渲染检查 select 的 value 和 state 更新确认 onChange 中使用了setPreset且组件读取了最新的preset页面出现横向滚动条动画位移导致内容溢出视口使用 DevTools 检查哪个元素宽度溢出给动画容器加overflow-x: hidden或调整位移距离9. 最佳实践与使用建议9.1 先定动画规范再写代码动画时长、缓动曲线、位移距离这些应该像颜色和字体一样成为设计令牌。建议在项目里维护一张动画规范表动画类型推荐时长缓动曲线位移距离页面级入场400mscubic-bezier(0.22, 1, 0.36, 1)24px卡片级反馈250msease-out8px列表增删250msease-out16px滚动叙事入场600mspower3.out48px这个表可能因设备和品牌风格不同而调整但一定要在团队内统一。9.2 组件化动画封装不要在每个页面里重复写initial、animate、exit。统一封装动画组件对外只暴露标题、内容、预设名称这几个 props。这样后续改动画时只改组件内部实现页面代码不用动。9.3 尊重系统动画设置在全局 CSS 里加入media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } }这对无障碍体验和兼容各类企业内网环境都重要。9.4 素材授权与隐私演示项目里的图标、插画、人物头像如果来自第三方或真实的用户数据必须审查授权范围。在企业内部做产品演示时不要直接使用真实客户名称和 Logo用占位数据替代。涉及用户生成内容的界面截图也要模糊敏感信息。9.5 发布前做动效评审动效评审不能只靠“看一眼”。建议准备一份 checklist动画时长是否在规范范围内、是否尊重减少动态效果、是否所有关键交互都有可感知反馈、低帧率设备上是否明显卡顿、页面切换时是否有闪烁。这个清单也可以集成到 CI 脚本里自动化检查配置项是否越界。10. 总结与下一步UI 交互动画在产品演示里的作用不只是“好看”而是把设计方案变成可以被评审、被理解、被验证的运行原型。这个项目里最值得尝试的是三点动画预设的集中配置、layout属性带来的列表重排效果、以及 GSAP ScrollTrigger 实现的滚动叙事。把这三点拆开每一部分都能直接搬到真实项目里。第一个应该验证的功能是 ListScene 的增删卡片因为它覆盖了 Framer Motion 最核心的入场、退场和布局动画代码量小但效果直观。最容易踩的坑是滚动动画的触发时机和组件卸载清理。ScrollTrigger 如果注册混乱会导致滚动时动画不触发或内存泄漏务必在useEffectcleanup 阶段调用ctx.revert()。后续可以扩展的方向接入设计系统的真实 design token让动画参数和颜色字体统一管理把整个演示工程接到 Storybook作为组件库的动效文档或者把滚动叙事方案直接搬到官网落地页替换成品牌内容。配置化、组件化、可测化这三件事做完UI 交互动画就不再是视觉装饰而是产品演示里真正有说服力的技术资产。