还在用 CSS 写过渡?Framer Motion 让 React 动画丝滑到犯规!

520 阅读5分钟

你是不是也遇到过这种情况:用 CSS 的transition做动画,简单的展开收起还行,但稍微复杂点的交互(比如列表添加删除动画、拖拽效果)就捉襟见肘,代码写得又长又乱?

其实,React 生态中有个专门解决动画问题的 “神器”——framer-motion。它能让你用几行代码实现原本需要几十行 CSS 才能搞定的动画,甚至能轻松实现拖拽、手势等复杂交互,让你的 React 应用从此告别 “PPT 式切换”。

CSS transition 的局限

用 CSS 的transition做简单动画确实方便,比如这个点击展开 / 收起的盒子:

// components/Box.jsx
import { useState } from 'react';
import styles from './box.module.css';

const Box = () => {
  const [open, setOpen] = useState(false);
  return (
    <div>
      <button onClick={() => setOpen(!open)}>
        {open ? '收起' : '展开'}
      </button>
      {/* 用CSS类名切换控制高度,配合transition实现动画 */}
      <div className={`${styles.box} ${open ? styles.open : ''}`} />
    </div>
  );
};
/* box.module.css */
.box {
  width: 100px;
  height: 0;
  background: lightblue;
  transition: height 0.3s ease; /* 定义过渡效果 */
  overflow: hidden;
}
.box.open {
  height: 100px; /* 展开时的高度 */
}

20250728-0340-46.4640532.gif 这个动画能跑起来,但如果需求变复杂,CSS 就很难应对了:

  • 想要 “先变宽再变高” 的序列动画,CSS 需要配合animation和关键帧,代码冗长;
  • 想要列表项删除时 “渐隐 + 上移” 的联动效果,CSS 几乎无法实现;
  • 想要给动画加 “弹性”“弹跳” 等物理效果,CSS 的ease函数不够用。

这时候,framer-motion的优势就体现出来了。

framer-motion:让 React 动画变得像写 JSX 一样简单

framer-motion是 React 生态中最流行的动画库之一,它的核心特点是声明式 API—— 你只需要描述 “动画的开始状态、结束状态和过渡方式”,剩下的交给库来处理。

(1)基础用法:从 “入场动画” 开始

先安装依赖:

pnpm i framer-motion

framer-motion实现一个 “元素挂载时渐入 + 上移” 的动画:

// components/MotionBox.jsx
import { motion } from 'framer-motion';

const MotionBox = () => {
  return (
    // 用motion组件包裹需要动画的元素
    <motion.div
      // 初始状态透明度0Y轴偏移-50px上方initial={{ opacity: 0, y: -50 }}
      // 目标状态透明度1Y轴偏移0正常位置animate={{ opacity: 1, y: 0 }}
      // 过渡配置持续0.5秒带弹性效果
      transition={{ duration: 0.5, type: 'spring', stiffness: 100 }}
      style={{ background: 'skyblue', padding: '20px' }}
    >
      <h2>我是带动画的盒子</h2>
    </motion.div>
  );
};

20250728-0405-45.2127885.gif

核心属性解析

  • motion:framer-motion 提供的 “动画化组件”,可以替代普通的divspan等,支持所有 HTML 标签(如motion.buttonmotion.ul)。
  • initial:元素初始状态(未动画时),通常用于定义 “入场前” 的状态。
  • animate:元素的目标状态,元素挂载后会自动从initial过渡到animate
  • transition:过渡配置,可指定动画时长(duration)、缓动类型(type: 'spring'表示弹性效果)等。

(2)状态切换动画:比 CSS 类名切换更灵活

framer-motion实现 “点击展开 / 收起”,支持更复杂的状态变化:

// components/AnimatedBox.jsx
import { useState } from 'react';
import { motion } from 'framer-motion';

const AnimatedBox = () => {
  const [open, setOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setOpen(!open)}>
        {open ? '收起' : '展开'}
      </button>
      {/* 用motion.div实现高度+背景色的联动动画 */}
      <motion.div
        // 初始状态高度0背景色浅蓝
        initial={{ height: 0, backgroundColor: '#add8e6' }}
        // 根据open状态动态切换目标状态
        animate={{ 
          height: open ? 100 : 0, 
          backgroundColor: open ? '#f5f5f5' : '#add8e6' 
        }}
        // 过渡配置0.3秒easeOut缓动
        transition={{ duration: 0.3, ease: 'easeOut' }}
        style={{ overflow: 'hidden' }}
      />
    </div>
  );
};

20250728-0417-15.5783478.gif

这个例子中,framer-motion自动处理了 “高度” 和 “背景色” 的同步过渡,无需像 CSS 那样手动写两个transition

(3)进阶:用variants组织复杂状态

当动画状态较多时(如 “默认”“hover”“激活”),可以用variants统一管理状态,让代码更清晰:

// components/ButtonWithVariants.jsx
import { motion } from 'framer-motion';

// 定义动画变体(不同状态的样式)
const variants = {
  default: { scale: 1, backgroundColor: '#fff' }, // 默认状态
  hover: { scale: 1.05, backgroundColor: '#f0f0f0' }, //  hover状态
  active: { scale: 0.95, backgroundColor: '#e0e0e0' } // 点击状态
};

const AnimatedButton = () => {
  return (
    <motion.button
      variants={variants} // 关联变体
      initial="default" // 初始状态对应variants.default
      whileHover="hover" // hover时自动切换到hover状态
      whileTap="active" // 点击时自动切换到active状态
      transition={{ type: 'spring', stiffness: 300 }} // 过渡配置
      style={{ 
        padding: '10px 20px', 
        border: '1px solid #ddd', 
        borderRadius: '4px',
        cursor: 'pointer'
      }}
    >
      点我试试
    </motion.button>
  );
};

20250728-0422-27.7042421.gif

variants的优势在于:状态逻辑与 UI 分离,方便复用和维护。

(4)列表动画:删除项时的 “联动效果”

framer-motionAnimatePresence组件,可以轻松实现 “组件卸载时的动画”,这在列表中非常实用:

// components/TodoList.jsx
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';

const TodoList = () => {
  const [todos, setTodos] = useState([
    { id: 1, text: '学习framer-motion' },
    { id: 2, text: '实现列表动画' }
  ]);

  const removeTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <div>
      {/* AnimatePresence用于检测子元素的挂载/卸载 */}
      <AnimatePresence>
        {todos.map(todo => (
          <motion.div
            key={todo.id}
            // 入场动画从下方滑入渐显
            initial={{ opacity: 0, y: 20 }}
            animate={{ opacity: 1, y: 0 }}
            // 离场动画渐隐向右滑出
            exit={{ opacity: 0, x: 20 }}
            transition={{ duration: 0.2 }}
            style={{ 
              padding: '10px', 
              border: '1px solid #eee', 
              margin: '5px 0',
              display: 'flex',
              justifyContent: 'space-between'
            }}
          >
            <span>{todo.text}</span>
            <button onClick={() => removeTodo(todo.id)}>删除</button>
          </motion.div>
        ))}
      </AnimatePresence>
    </div>
  );
};

20250731-0219-53.9656481.gif

当删除列表项时,被删除的项会 “渐隐 + 右滑” 消失,剩下的项会自动上移补位,整个过程流畅自然 —— 这用 CSS 几乎无法实现。

(5)物理效果:让动画更 “真实”

framer-motion支持多种物理动画类型(如spring弹簧、inertia惯性),让动画更贴近真实世界的运动规律:

// components/BouncyBox.jsx
import { motion } from 'framer-motion';

const BouncyBox = () => {
  return (
    <motion.div
      // 初始状态Y轴偏移-200px上方initial={{ y: -200 }}
      // 目标状态Y轴偏移0落下animate={{ y: 0 }}
      // 弹簧效果stiffness刚度越小弹跳越明显
      transition={{ type: 'spring', stiffness: 50, damping: 10 }}
      style={{ 
        width: 100, 
        height: 100, 
        backgroundColor: 'pink',
        borderRadius: '8px'
      }}
    />
  );
};

20250731-0221-51.2607508.gif 这个盒子会像 “弹簧球” 一样落下并轻微弹跳,比 CSS 的ease效果生动得多。

入门必知:Framer Motion 核心概念

  • motion组件:替代普通 HTML 标签(divmotion.divbuttonmotion.button),所有动画属性都加在这上面;
  • initial:初始状态(动画开始前的样式);
  • animate:目标状态(动画要达到的样式);
  • transition:过渡规则(时长、曲线、延迟等);
  • variants:动画变体,集中管理多个状态的样式,方便复用;
  • AnimatePresence:处理元素 “退场” 动画(必须包裹动态增删的元素)。

总结:从 CSS 到 framer-motion 的选择指南

  • 简单的属性过渡(如 hover 时颜色变化):用 CSS transition足够,轻量高效;
  • 复杂的状态切换、序列动画、列表动画:用framer-motion,节省开发时间;
  • 需要物理效果、手势交互:必选framer-motion,没有更简单的方案。