Framer Motion

Framer Motion

用于构建流畅、生产级 UI 动画的 React 动画库

版本:12.23.24

官网:https://motion.dev/

安装

  1. 安装依赖

    1
    npm install motion
  2. 在代码中使用

    1
    import { motion } from "motion/react"
  3. 测试使用

    1. 新建个项目

    2. 创建个组件

      一个旋转的正方体

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      import * as motion from "motion/react-client"

      export default function MyHome() {
      return (
      <div style={container}>
      <Rotate/>
      </div>
      )
      }

      function Rotate() {
      return (
      <motion.div
      style={box}
      animate={{ rotate: 360 }}
      transition={{ duration: 1 }}
      />
      )
      }

      const box = {
      width: 100,
      height: 100,
      backgroundColor: "#ff0088",
      borderRadius: 5,
      }

      const container = {
      // 元素撑满整个宽度
      width: "100vw",
      // 元素撑满整个高度
      height: "100vh",
      // 弹性布局容器, 开启 flex 后才能使用对齐方式
      display: "flex",
      // 垂直方向居中
      alignItems: "center",
      // 水平方向居中
      justifyContent: "center",
      }
    3. 修改路由配置router.ts

      1
      2
      3
      import { type RouteConfig, index } from "@react-router/dev/routes";

      export default [index("routes/my_home.tsx")] satisfies RouteConfig;

动画

通过<motion.HTML元素 />来使用一系列特殊的动画属性

官方文档

常见动画属性

  • animate:传递给 animate 的值发生变化时,元素将自动动画到该值

    1
    <motion.div animate={{ rotate: 360 }} />
  • initial:元素设置初始值

    1
    2
    3
    4
    <motion.article
    initial={{ opacity: 0 }}
    animate={{ opacity: 1 }}
    />
  • originX/originY/originZ:设置做变换的原点,默认值是0.5

    1
    <motion.div style={{ originX: 0.5 }} />
  • transition:定义动画的过渡效果

    1
    2
    3
    4
    <motion.div
    animate={{ x: 100 }}
    transition={{ ease: "easeOut", duration: 2 }}
    />

    通过MotionConfig可以给多个子组件设置默认的过渡效果

    1
    2
    3
    <MotionConfig transition={{ duration: 0.3 }}>
    <motion.div animate={{ opacity: 1 }} />
    ....
    ease的取值 效果
    linear 匀速
    easeIn 先慢后快
    easeOut 先快后慢
    easeInOut 慢 → 快 → 慢
    backIn 往后缩一下再冲出去
    backOut 冲过头一点再回来
    backInOut 两边都带一点回弹
    自定义贝塞尔曲线 ease: [0.42, 0, 0.58, 1]

    在线贝塞尔曲线网站:https://cubic-bezier.com/

  • exit:当组件被移除时的动画

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <AnimatePresence>
    {isVisible && (
    <motion.div
    key="modal"
    initial={{ opacity: 0 }}
    animate={{ opacity: 1 }}
    exit={{ opacity: 0 }}
    />
    )}
    </AnimatePresence>

动画关键帧

传入的值为数组时,会按顺序通过这些值进行动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<motion.div
style={box}
animate={{
scale: [1, 2, 2, 1, 1],
rotate: [0, 0, 180, 180, 0],
borderRadius: ["0%", "0%", "50%", "50%", "0%"],
}}
transition={{
duration: 2,
ease: "easeInOut",
times: [0, 0.2, 0.5, 0.8, 1], // 关键帧占整个动画的百分比
repeat: Infinity, // 无限循环
repeatDelay: 1, // 一轮过后挺1s
}}
/>

注意:

  • 数组中的值为null时表示使用当前值

    1
    2
    // null的时候为2
    scale: [1, 2, 2, null, 1],
  • times是一个介于 0和1之间的进度值数组,用于定义每个关键帧在动画中的位置

交互动画

常用的手势交互动画有:

  • whileHover:悬停时
  • whileTap:按下时
  • whileFocus:聚焦时
  • whileDrag:拖拽时
  • whileInView:滚动进入视口时

手势动画结束后会回到animate或initial的值

交互时过渡效果及事件处理

1
2
3
4
5
6
7
8
9
10
11
12
<motion.button
whileHover={{
scale: 1.1,
// 定义悬停时的过渡效果
transition: { duration: 0.1 }
}}
// 还可以定义对应的事件处理
onHoverStart={() => console.log('Hover starts')}
onHoverEnd={() => console.log('Hover ends')}
// 默认的过渡效果
transition={{ duration: 0.5 }}
/>

拖拽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<motion.div
drag
style={{
width: 100,
height: 100,
backgroundColor: "#dd00ee",
borderRadius: 10,
}}
whileDrag={{
scale: 1.1,
boxShadow: "0px 10px 20px rgba(0,0,0,0.2)"
}}
>
</motion.div>

注意点:

  • 加上drag后就可以拖动,可以指定能拖动的方向drag="x"
  • whileDrag为拖动的时候加上的样式
  • dragMomentum={false}可以去掉放开后的惯性
  • dragTransition={{}}可以自定义惯性效果
  • dragConstraints={}设置可拖拽的容器
  • dragElastic={}可设置超出容器回弹的力度,值在[0, 1]之间
  • 常用到的相关事件有onDragStartonDragonDragEnd

✨定义变量

通过定义变量实现复用和方便管理

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义变量
const variants = {
visible: { opacity: 1 },
hidden: { opacity: 0 },
}

// 使用
<motion.div
variants={variants} // 引入变量
initial="hidden"
whileInView="visible"
exit="hidden"
/>

使用变量还可以控制父子动画的间隔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const list = {
visible: {
opacity: 1,
transition: {
when: "beforeChildren", // 在子动画之前
delayChildren: stagger(0.3), // 和子动画错开0.3s
},
},
hidden: {
opacity: 0,
transition: {
when: "afterChildren", // 在子动画之后
},
},
}

变量还可以定义为函数

1
2
3
4
5
6
7
8
9
10
const variants = {
hidden: { opacity: 0 },
visible: (index) => ({
opacity: 1,
transition: { delay: index * 0.3 }
})
}

// 通过custom传入参数
items.map((item, index) => <motion.div custom={index} variants={variants} />)

过渡

transition 定义了在两个值之间动画时使用的动画类型

https://motion.dev/docs/react-transitions

过渡类型

1
2
3
4
<motion.path
animate={{ pathLength: 1 }}
transition={{ duration: 2, type: "tween" }}
/>

type可以取:

  • tween:根据持续时间duration和缓动ease来动
  • spring:类似弹簧
  • inertia:惯性

定义动画帧的位置

1
2
3
4
5
6
<motion.div
animate={{
x: [0, 100, 0],
transition: { times: [0, 0.3, 1] } // 值在[0, 1]
}}
/>

布局动画

layout

加上layout后,DOM的大小、位置、布局变化时也会触发动画过渡效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 按钮
<button
className="toggle-container"
style={{
...container,
justifyContent: "flex-" + (isOn ? "start" : "end"),
}}
onClick={toggleSwitch}
>
<motion.div
className="toggle-handle"
style={handle}
layout // 当点击这个按钮时布局会从左对齐变成右对齐会触发过渡效果
transition={{
type: "spring",
visualDuration: 0.2,
bounce: 0.2,
}}
/>
</button>

如果要专门给layout设置过渡动画,可以这么做:

1
2
3
4
5
6
7
8
<motion.div
layout
animate={{ opacity: 0.5 }}
transition={{
ease: "linear",
layout: { duration: 0.3 } // 给layout专门设置
}}
/>

注意:

  • 可滚动的元素是加layoutScroll

    1
    2
    3
    4
    <motion.div 
    layoutScroll
    style={{ overflow: "scroll" }} // 当里面的内容超过容器大小时,强制出现滚动条
    />
  • 固定的元素是加layoutRoot

    1
    2
    3
    4
    <motion.div 
    layoutRoot
    style={{ position: "fixed" }} // 不受父元素影响,直接按照浏览器窗口定位
    />

layoutId

在两个不同组件之间实现动画过渡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 大小卡片之间切换时平滑过渡
function Card() {
const [selected, setSelected] = useState(false);
return (
<div>
{selected ? (
<BigCard setSelected={setSelected} />
) : (
<SmallCard setSelected={setSelected} />
)}
</div>
)
}

function SmallCard({ setSelected } : any) {
return (
<motion.div
layoutId="card"
onClick={() => setSelected(true)}
style={{
width: 100,
height: 100,
borderRadius: 20,
background: "#4CC9F0"
}}
/>
);
}

function BigCard({ setSelected } : any) {
return (
<motion.div
layoutId="card"
onClick={() => setSelected(false)}
style={{
width: 300,
height: 400,
borderRadius: 30,
background: "#4CC9F0"
}}
/>
);
}

滚动动画

进入窗口时触发

  • whileInView:进入窗口时的动画

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    function Test() {
    return(
    <div
    style={{height: "10000px"}} // 让窗口长一点
    >
    <motion.div
    style={{
    background: "#c90b0b",
    width: "100px",
    height: "100px",
    borderRadius: "0%",
    }}
    initial={{ opacity: 0 }}
    whileInView={{ // 每次元素进入窗口时都会触发,出窗口会变回原来的值
    opacity: 1,
    borderRadius: "50%",
    rotate: 360
    }}
    transition={{ duration: 0.5}}
    >
    </motion.div>
    </div>
    )
    }
  • viewport:设置为只会触发一次

    1
    2
    3
    4
    5
    <motion.div
    initial="hidden"
    whileInView="visible"
    viewport={{ once: true }}
    />

和滚动进度值关联

  • 获取滚动的值

    // scrollX:滚动的像素值,scrollXProgress:[0, 1]之间的进度值

    // container:在哪滚动

    // target:监听的元素,默认是监听窗口的滚动

    // offset:定义什么时候为0,什么时候为1

    // 如offset: [“start end”, “end start”]

    // 表示当元素顶部碰到视口底部时为 0,当元素底部碰到视口顶部时为 1

    const {scrollX, scrollY, scrollXProgress, scrollYProgress} = useScroll({container, target, offset})

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import {motion, useScroll} from "motion/react";

    // 范围为[0, 1],随着滚动的深度而增加
    const { scrollYProgress } = useScroll()

    // 随着滚动深度而增长的简易进度条
    <motion.div
    id="scroll-indicator"
    style={{
    scaleX: scrollYProgress, // 长度的缩放和滚动值绑定
    position: "fixed",
    top: 0,
    left: 0,
    right: 0,
    height: 10,
    originX: 0,
    backgroundColor: "#ff0088",
    }}
    />

    判断用户是往上还是往下滚动

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import {motion, useMotionValueEvent, useScroll} from "motion/react";

    const { scrollY } = useScroll()
    const [scrollDirection, setScrollDirection] = useState("down")

    useMotionValueEvent(scrollY, "change", (current) => {
    const diff = current - scrollY.getPrevious() // 现在的值和先前值的差
    setScrollDirection(diff > 0 ? "down" : "up")
    })
  • 让进度值的变动有过渡效果

    1
    2
    3
    4
    5
    6
    const { scrollYProgress } = useScroll()
    const scaleX = useSpring(scrollYProgress, {
    stiffness: 100,
    damping: 30,
    restDelta: 0.001
    })
  • 将进度值转化成其他值

    1
    2
    3
    4
    5
    6
    // 颜色变化是平滑变化的
    const backgroundColor = useTransform(
    scrollYProgress,
    [0, 0.5, 1],
    ["#f00", "#0f0", "#00f"]
    )

实例

  • 控制滚动的速度和方向

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    import {
    motion,
    useMotionValue,
    useAnimationFrame,
    useTransform, useScroll, useVelocity, useSpring
    } from "framer-motion";
    import {useRef, useState} from "react";
    import {handle, c} from "~/styles/my_home";
    import { wrap } from "@motionone/utils";

    export default function MyHome() {
    return (
    <section style={{
    background: "#3739fd",
    }}>
    <ParallaxText baseVelocity={-5}>Framer Motion</ParallaxText>
    <ParallaxText baseVelocity={5}>Scroll velocity</ParallaxText>
    </section>
    )
    }

    function ParallaxText({ children, baseVelocity = 100 } : any){
    // 文字x轴的位移值
    const baseX = useMotionValue(0)
    // 实现循环滚动
    const x = useTransform(baseX, (v) => `${wrap(-5, -30, v)}%`)

    // 获取滚动速度
    const { scrollY } = useScroll()
    const scrollVelocity = useVelocity(scrollY);
    // 让滚动速度平滑
    const smoothVelocity = useSpring(scrollVelocity, {
    damping: 50,
    stiffness: 400
    });
    // 把滚动速度映射为小的值
    const velocityFactor = useTransform(smoothVelocity, [0, 1000], [0, 5], {
    clamp: false,
    })

    // 运动方向
    const directionFactor = useRef(1)

    // 每一帧都会执行这个回调函数
    useAnimationFrame((t, delta) => {
    // delta: 两帧之间的间隔
    let moveBy = directionFactor.current * baseVelocity * delta / 1000

    // 根据滚动方向改变文字的运动方向
    if(velocityFactor.get() < 0) {
    directionFactor.current = -1
    } else if(velocityFactor.get() > 0) {
    directionFactor.current = 1
    }

    moveBy += directionFactor.current * moveBy * velocityFactor.get()

    baseX.set(baseX.get() + moveBy)
    })

    return(
    <div className="parallax">
    <motion.div
    className="scroller"
    style={{
    x: x,
    color: "#ffffff"
    }}
    >
    <span>{ children }</span>
    <span>{ children }</span>
    <span>{ children }</span>
    <span>{ children }</span>
    </motion.div>
    </div>
    )
    }

矢量图动画

SVG,Scalable Vector Graphics,可缩放矢量图

https://motion.dev/docs/react-svg-animation

画矢量

motion.circlemotion.linemotion.rect等来画矢量

支持属性pathLength来绘制线条

  • 画一个圆圈

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    <motion.svg			// 就像定义了一块画布来画SVG
    width="600"
    height="600"
    viewBox="0 0 600 600"
    initial="hidden"
    animate="visible"
    style={image}
    >
    <motion.circle
    className="circle-path"
    cx="100"
    cy="100"
    r="80"
    stroke="#ff0088"
    variants={draw}
    custom={1} // 延迟
    style={shape}
    />
    </motion.svg>


    const draw: Variants = {
    // 初始状态:完全透明,且路径长度为0(没画)
    hidden: { pathLength: 0, opacity: 0 },

    // 结束状态:接收一个参数 i (index)
    visible: (i: number) => {
    const delay = i * 0.5; // 核心:根据传入的 i 计算延迟时间
    return {
    pathLength: 1, // 画满
    opacity: 1, // 变不透明
    transition: {
    // pathLength 使用弹簧动画(spring),时长1.5秒,没有回弹(bounce: 0)
    pathLength: { delay, type: "spring", duration: 1.5, bounce: 0 },
    opacity: { delay, duration: 0.01 },
    },
    }
    },
    }

    // Styles
    const shape: React.CSSProperties = {
    strokeWidth: 10, // 线条宽度
    strokeLinecap: "round", // 线条末端是圆头的
    fill: "transparent", // 线条间填充是透明的,不写的话就是个实心圆了
    }

Framer Motion
http://xwww12.github.io/2025/11/19/前端/CSS库/FramerMotion/
作者
xw
发布于
2025年11月19日
许可协议