使用 Framer Motion 为 React 应用打造文本与图片动画:Refine 项目实战指南
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
本文以 Refine 仓库官方博客中的 Framer Motion 教程为主体,系统讲解如何在 React 应用中用极少量代码实现文本入场动画、悬停反馈以及图片旋转轮播、拖拽与点按缩放等交互动画,并对照 Refine 文档站源码(documentation/package.json 与 documentation/src/refine-theme 等)展示该库在真实项目中的落地方式。读完本文,你将掌握motion组件、animate/initial/transition/whileHover/whileTap/drag等核心 props 的用法,并能直接复制代码到自己的 React 项目中运行。
Framer Motion 是什么
Framer Motion 是 Framer 团队推出的、可投入生产环境的 React 动画库,它能够直接在 DOM 元素上创建多种动画效果,是编写复杂 CSS 动画的优秀替代方案。相比手写大量 CSS keyframes,Framer Motion 把动画声明收敛为组件上的 props,让开发者把精力集中在业务本身。
在使用它之前,需要在 React 项目中安装依赖并导入所需组件。
环境准备
跟随本文动手实践前,需要具备:
- 对 HTML、CSS、JavaScript 基础前端技术有清晰理解;
- 了解 React.js 基础知识;
- 本机已安装 Node.js。
React 项目中的外部库统一通过包管理器安装,执行:
npm install framer-motion安装完成后,在需要使用动画的组件中导入 Framer Motion 的组件即可开始编码。本文内容分为两部分:先完成文本动画,再完成图片动画,最后两者共同组成一个完整的 React 应用。
文本动画:用 motion 组件让标题动起来
认识 motion 组件
在App.js中加入如下代码:
import { motion } from "framer-motion"; function App() { return ( <div className="App"> <motion.h1 animate={{ x: [50, 150, 50], opacity: 1, scale: 1 }} transition={{ duration: 5, delay: 0.3, ease: [0.5, 0.71, 1, 1.5], }} initial={{ opacity: 0, scale: 0.5 }} whileHover={{ scale: 1.2 }} > Animation made easy with Framer Motion </motion.h1> </div> ); } export default App;注意这里的h1与标准h1元素的差别:它使用了motion前缀。motion组件是使用 Framer Motion 制作动画的第一步,它可以配合任意标准 HTML 元素使用(motion.div、motion.h1、motion.button等),其本质是把 HTML 元素封装成 React 组件,再通过 props 驱动动画。
initial 与 animate:声明动画的起点与终点
为了让文本在页面加载时拥有一个渐显的入场动画,我们同时使用了initial和animate两个 props:
initial定义元素的初始状态,这里设置为{ opacity: 0, scale: 0.5 },即一开始文字透明且缩小到一半;animate定义元素的动画目标状态,这里设置为{ opacity: 1, scale: 1 },即最终完全显示并恢复原始大小。
Framer Motion 会自动计算并补间这两个状态之间的过渡。此外,animate还支持传入关键帧数组,让元素按顺序经过多个值。代码中的x: [50, 150, 50]表示文本沿 x 轴从 50 移动到 150,再回到 50(原文中给出的写法为x: [0, 150, 50],效果等价,均表示一个往返移动)。opacity取 1、scale取 1,当缩放达到 1 时文本停止增大尺寸,避免“狂野”的视觉效果。
transition:控制动画节奏
每一个有效的动画都离不开过渡过程。transitionprop 用来描述动画如何播放,包括三个关键参数:
duration:动画持续时长(秒),示例中为 5;delay:动画开始前的延迟时长(秒),示例代码中为 0.3(原文叙述中写作 3,以代码 0.3 为准可让入场衔接更自然);ease:缓动函数。示例使用贝塞尔曲线数组[0.5, 0.71, 1, 1.5](原文叙述中写作[0.5, 0.71, 1, 1.01]),它描述了文本动画在进入原始缩放值之前以怎样的速率进行,改变数组即可获得不同的“弹入”节奏。
参数一致性提示:原文档正文对
delay与ease数值的描述和代码示例略有出入,实际运行时以组件代码中的取值为准。这也提醒我们,动画参数往往需要肉眼微调,建议以transition中的代码为准进行调试。
whileHover:悬停放大效果
光有入场动画还不够,我们再为文本加上悬停反馈:当鼠标悬停在文字上时,文字像被“弹起”一样略微变大。
function App() { return ( <div className="App"> <motion.h1 ... // highlight-next-line whileHover={{ scale: 1.2 }} > Animation made easy with Framer Motion </motion.h1> </div> ); }whileHover是 Framer Motion 的手势(gesture)props之一,它声明元素在悬停状态下应呈现的目标样式。这里scale: 1.2表示悬停时放大到原始尺寸的 1.2 倍,松开鼠标后自动回到原状,无需手动编写任何事件监听或 CSS 类切换。
图片动画:构建带旋转、拖拽与点按反馈的轮播
组织图片资源
首先在src目录下创建images文件夹存放图片,然后在src下新建image.js,集中导入所有图片:
import fox from "./images/pexels-fox-1071249.jpg"; import jill from "./images/jill.jpg"; import cha1 from "./images/cha1.jpg"; import cha2 from "./images/cha2.jpg"; export default [fox, jill, cha1, cha2];把所有图片统一收拢到image.js再导出数组,可以避免在App.js中反复 import 同一批图片,减少冗余代码。
编写样式
在App.css中加入轮播所需的基础样式:
.item { min-height: 10rem; min-width: 30rem; padding: 5px; } .item img { width: 60%; height: 50%; border-radius: 1rem; pointer-events: none; padding: 15%; } .inner-carousel { display: flex; background: rgb(76, 76, 76); height: 23rem; } .carousel { cursor: grab; overflow: hidden; background: rgb(215, 216, 215); height: 23rem; }其中.carousel负责裁剪溢出内容并呈现“手抓”光标,.inner-carousel使用 flex 排列图片,.item img中的pointer-events: none避免图片自身拦截拖拽事件。
三层容器结构与旋转动画
修改App.js为如下代码,实现一个带旋转效果的轮播:
import { motion } from "framer-motion"; import "./App.css"; import images from "./image"; function App() { return ( <div className="App"> <motion.div className="carousel"> <motion.div animate={{ scale: [1, 1, 1, 1, 1], rotate: [0, 30, 60, 240, 360], }} > {images.map((image) => { return ( <motion.div whileHover={{ scale: 1.2 }} whileTap={{ scale: 2 }} className="item" transition={{ duration: 3 }} key={image} > <img src="https://refine.ams3.cdn.digitaloceanspaces.comundefined" alt="Carousel image placeholder" /> </motion.div> ); })} </motion.div> </motion.div> </div> ); } export default App;整个动画的形态是:页面每次刷新时轮播执行一次旋转,鼠标靠近图片时触发悬停效果,触摸图片时触发点按效果。实现上需要三个容器,从外到内依次是外层轮播容器、内层旋转容器、图片容器,三者全部使用 Framer Motion 的motion组件层层嵌套:
- 内层容器承担旋转与滑动效果,通过
animateprop 将scale设为[1, 1, 1, 1, 1]、rotate设为[0, 30, 60, 240, 360],让图片组完成一整圈 360° 的独特旋转; - 最内层图片容器负责逐张渲染从
image.js导入的图片。
占位说明:原文示例中
<img src>使用了占位 URL,实际接入时请改为src={image}引用images数组中映射出的真实图片地址,即可正确显示轮播内容。
拖拽与回弹:drag 系列 props
为了让轮播具备“可拖拽”的类轮播手感,给内层旋转容器加上drag与dragConstraints:
function App() { return ( <div className="App"> <motion.div className="carousel"> <motion.div ... // highlight-start drag="x" dragConstraints={{right: 0, left:-1100}} dragTransition={{ bounceStiffness: 600, bounceDamping: 8 }} // highlight-end > ... </motion.div> </motion.div> </div> ); }drag="x":只允许沿 x 轴(水平方向)拖拽,因为轮播需要左右滑动;dragConstraints={{ right: 0, left: -1100 }}:设置拖拽边界,让轮播向左最多移动 1100px,随后返回到原位,相当于一个行程挡板;dragTransition={{ bounceStiffness: 600, bounceDamping: 8 }}:为拖拽末端增加弹跳感。bounceStiffness控制回弹的“刚性”(600 表示回弹较有力度),bounceDamping控制回弹阻尼(8 表示衰减较快、摆动几次即稳定),两者配合即可在每次拖到尽头时呈现自然的橡皮筋回弹效果。
悬停与点按:whileHover 与 whileTap
轮播中最内层的图片容器用于承载图片并定义交互动画:
function App() { return ( <div className="App"> <motion.div className="carousel"> <motion.div> {images.map((image) => { return ( <motion.div ... //highlight-start whileHover={{ scale: 1.2 }} whileTap={{ scale: 2 }} //highlight-end > <img src="https://refine.ams3.cdn.digitaloceanspaces.comundefined" alt="Carousel image placeholder" /> </motion.div> ); })} </motion.div> </motion.div> </div> ); }whileHover={{ scale: 1.2 }}:鼠标悬停时图片最多放大到 1.2 倍;whileTap={{ scale: 2 }}:按下/触摸时放大到 2 倍,释放后回弹,形成活泼的“点按弹跳”反馈;- 同时为该容器设置
transition={{ duration: 3 }}(示例取值,可按需缩短至 0.3~0.5 秒获得更跟手的交互),让悬停与点按动画更平滑自然。
与纯 CSS 相比,以上旋转、拖拽、回弹、悬停、点按效果全部由几行 props 声明完成,无需维护复杂的@keyframes与手势状态机,这正是 Framer Motion 的核心价值。
仓库实证:Refine 文档站中的 Framer Motion 应用
Framer Motion 并非只在教程里出现,Refine 仓库的文档站点本身就重度使用它。这为理解该库的“真实项目用法”提供了第一手源码证据。
依赖版本
在 documentation/package.json 中可以看到文档站依赖声明"framer-motion": "^6.5.1",即当前仓库使用的是 Framer Motion 6.x 版本线。
博客阅读进度条:useScroll + useTransform
documentation/src/refine-theme/blog-header.tsx 中的BlogHeader组件演示了典型的滚动联动动画:
import { motion, useScroll, useTransform } from "framer-motion"; // ... const { scrollYProgress } = useScroll(); const progressPercentage = useTransform( scrollYProgress, [0.03, 0.95], ["0%", "100%"], );useScroll()返回页面滚动进度 motion value,useTransform将其映射为百分比字符串,再通过style={{ width: progressPercentage }}绑定到motion.div上,形成随滚动推进的顶部进度条。这与教程中“把动画值绑定到元素样式”的思路一脉相承。
首页能力轮播:useMotionValue + useTransform + animate
documentation/src/refine-theme/landing-magic.tsx 中的LandingMagic组件在首页用useMotionValue(0)维护序列值,再用useTransform把序列映射为纵向位移百分比["0%", "-...%"],最后通过命令式animate(sequence, nextValue, { type: "spring", bounce: 0.35, stiffness: 50 })驱动一组数据源/UI 库图标循环滚动,效果与教程中的旋转轮播异曲同工——都用 motion value 驱动 transform 变换。
数字滚动计数器:animate + onUpdate
documentation/src/hooks/use-animated-counter.tsx 封装了一个useAnimatedCounterhook:
const controls = animate(initialValue, maxValue, { duration, ease: "easeOut", onUpdate(value) { setCounter(value); }, }); return () => controls.stop();它用animate()在指定时长内从初始值插值到目标值,每帧通过onUpdate回调刷新 React state,并在卸载时调用controls.stop()清理动画。这是 Framer Motion 在非视觉、数值型动画场景下的典型用法。
抽屉与滚动偏移:useScroll
documentation/src/refine-theme/common-drawer.tsx 中的抽屉组件通过useScroll()监听scrollY,用scrollY.onChange动态计算顶部偏移量,保证抽屉在滚动条移动时仍然对齐公告栏——印证了 motion value 的可订阅特性在真实布局逻辑中的作用。
此外,documentation/src/refine-theme 目录下的doc-survey-widget.tsx、doc-thumbs-up-down-feedback-widget.tsx、landing-sweet-spot.tsx等组件还导入了AnimatePresence、motion、useInView,用于反馈小组件的进出场动画与视口检测,说明AnimatePresence(列表项/条件渲染元素的离场动画)和useInView(元素进入视口时触发动画)同样是日常项目的高频能力。
Framer Motion 常用 props 速查
结合教程与仓库源码,将本文涉及的 props 归纳如下:
| Prop | 作用 | 本文示例取值 |
|---|---|---|
motion.* | 将任意 HTML 元素包装为可动画组件 | motion.h1、motion.div |
initial | 定义动画的初始状态 | { opacity: 0, scale: 0.5 } |
animate | 定义动画目标状态或关键帧序列 | { x: [50, 150, 50], opacity: 1, scale: 1 }、{ rotate: [0, 30, 60, 240, 360] } |
transition | 控制时长、延迟与缓动 | { duration: 5, delay: 0.3, ease: [0.5, 0.71, 1, 1.5] } |
whileHover | 悬停手势的目标状态 | { scale: 1.2 } |
whileTap | 点按/触摸手势的目标状态 | { scale: 2 } |
drag | 启用拖拽并限定轴向 | "x" |
dragConstraints | 限制拖拽边界 | { right: 0, left: -1100 } |
dragTransition | 拖拽回弹物理参数 | { bounceStiffness: 600, bounceDamping: 8 } |
useScroll | 订阅页面/元素滚动进度 | const { scrollYProgress } = useScroll() |
useTransform | 将一个 motion value 映射为另一个 | useTransform(scrollYProgress, [0.03, 0.95], ["0%", "100%"]) |
useMotionValue | 创建可订阅的数值源 | useMotionValue(0) |
animate | 命令式地驱动动画 | animate(0, 100, { duration, onUpdate }) |
总结
本文从零开始演示了 Framer Motion 在 React 应用中的两种典型动画:用motion组件配合initial/animate/transition/whileHover完成的文本入场与悬停放大,以及用嵌套motion容器配合animate旋转关键帧、drag/dragConstraints/dragTransition拖拽回弹、whileHover/whileTap缩放反馈构建的图片轮播。相比手写 CSS 动画,这些效果仅需寥寥数行声明式 props 即可完成。
同时,Refine 文档站源码(blog-header.tsx、landing-magic.tsx、use-animated-counter.tsx、common-drawer.tsx)证明了这些 API 在生产环境中的真实价值:进度条、轮播、计数器、抽屉偏移乃至进离场动画,都可借助 Framer Motion 以声明式、低样板的方式落地。Framer Motion 还提供 Variants、AnimatePresence、useInView、useSpring、useVelocity等更强大的能力,值得在此基础上继续探索。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考