2、CSS过渡与动画基础:从静态到动态的第一步

说实话,CSS动画是我在React项目中用得最多的技术。为什么?因为它简单、高效,而且浏览器原生支持。你想想看,一个简单的淡入效果,用JS库可能要写几十行代码,但CSS几行就搞定了。

这一章,我会带你从最基础的CSS过渡开始,一步步深入到@keyframes动画,最后在React中实战一个淡入淡出组件。嗯,这些都是我实际项目中反复用到的套路。

2.1 CSS transition属性详解

transition,说白了就是「状态变化的平滑过渡」。比如一个按钮,鼠标移上去颜色从蓝变红,直接跳变太生硬,加上transition就丝滑了。

transition有四个子属性,我习惯这样记:

属性 作用 示例值
transition-property 指定哪些属性要过渡 all, opacity, transform
transition-duration 过渡持续多久 0.3s, 300ms
transition-timing-function 速度曲线 ease, linear, cubic-bezier()
transition-delay 延迟多久开始 0s, 0.1s

我在项目中遇到过一个问题:用transition: all 0.3s虽然方便,但性能很差。尤其是改变宽高、位置这些会触发重排的属性,掉帧严重。所以我建议,尽量只过渡opacity和transform,这两个属性不会触发重排。

最佳实践:transition只用在opacity和transform上,性能最好。

来看一个完整的例子:

.fade-box {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.3s ease, transform 0.3s ease;
}

.fade-box.visible {
  opacity: 1;
  transform: translateY(0);
}

这里我用了两个属性同时过渡,效果就是「淡入 + 上移」。你想想看,如果只用opacity,元素只是原地出现,加上translateY就多了一层「从下方滑入」的感觉,用户体验好很多。

小技巧:transition的简写顺序是「属性 时长 速度曲线 延迟」,比如transition: opacity 0.3s ease 0.1s

2.2 CSS animation与@keyframes

transition只能处理「开始」和「结束」两个状态。但有时候我们需要更复杂的动画,比如「先放大再缩小再恢复」,这时候就要用animation + @keyframes了。

@keyframes可以定义多个关键帧,每个关键帧里可以设置不同的样式。我习惯把它想象成一个「动画剧本」:

@keyframes fadeInUp {
  0% {
    opacity: 0;
    transform: translateY(20px);
  }
  50% {
    opacity: 0.5;
    transform: translateY(10px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}

然后通过animation属性来调用:

.element {
  animation: fadeInUp 0.5s ease forwards;
}

animation的属性比transition多一些,我列个表:

属性 说明 常用值
animation-name 指定@keyframes名称 fadeInUp, slideIn
animation-duration 动画时长 0.5s, 1s
animation-timing-function 速度曲线 ease, ease-out
animation-delay 延迟时间 0s, 0.2s
animation-iteration-count 播放次数 1, infinite
animation-direction 播放方向 normal, reverse, alternate
animation-fill-mode 结束后状态 none, forwards, backwards

注意:animation-fill-mode: forwards 很常用,它让动画结束后保持最后一帧的状态。如果不加,动画结束后元素会跳回初始状态。

我曾经踩过一个坑:用animation做无限循环的旋转动画,结果忘记加animation-fill-mode: forwards,每次循环结束元素都会闪一下。排查了半天才发现是这个问题。

2.3 在React中应用CSS动画

在React里用CSS动画,其实和普通HTML差不多。但有一个关键点:React控制的是类名的切换

我常用的模式是这样的:

import React, { useState } from 'react';
import './FadeBox.css';

function FadeBox() {
  const [visible, setVisible] = useState(false);

  return (
    <div>
      <button onClick={() => setVisible(!visible)}>
        切换
      </button>
      <div className={`fade-box ${visible ? 'visible' : ''}`}>
        我是内容
      </div>
    </div>
  );
}

CSS部分就是我们之前写的:

.fade-box {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.3s ease, transform 0.3s ease;
}

.fade-box.visible {
  opacity: 1;
  transform: translateY(0);
}

你看,核心逻辑就是「通过React状态控制类名,CSS负责动画」。这种方式的好处是:动画逻辑和业务逻辑完全分离

核心思想:React管状态,CSS管动画。各司其职,互不干扰。

2.4 实战:淡入淡出组件

好了,理论说完了,我们来写一个真正的可复用组件。这个组件我封装过很多次,每次项目里都会用到。

先看最终效果:

// FadeIn.jsx
import React, { useState, useEffect } from 'react';
import './FadeIn.css';

function FadeIn({ children, show, duration = 300 }) {
  const [shouldRender, setShouldRender] = useState(show);

  useEffect(() => {
    if (show) {
      setShouldRender(true);
    } else {
      // 等动画结束后再移除DOM
      const timer = setTimeout(() => {
        setShouldRender(false);
      }, duration);
      return () => clearTimeout(timer);
    }
  }, [show, duration]);

  return (
    <div
      className={`fade-in ${show ? 'enter' : 'exit'}`}
      style={{ '--duration': `${duration}ms` }}
    >
      {shouldRender && children}
    </div>
  );
}

export default FadeIn;

CSS部分:

.fade-in {
  transition: opacity var(--duration) ease, transform var(--duration) ease;
}

.fade-in.enter {
  opacity: 1;
  transform: translateY(0);
}

.fade-in.exit {
  opacity: 0;
  transform: translateY(10px);
}

使用方式:

function App() {
  const [show, setShow] = useState(true);

  return (
    <div>
      <button onClick={() => setShow(!show)}>切换</button>
      <FadeIn show={show} duration={500}>
        <div style={{ padding: 20, background: '#f0f0f0' }}>
          我是淡入淡出的内容
        </div>
      </FadeIn>
    </div>
  );
}

设计思路:为什么要有shouldRender?因为退出动画需要时间,如果直接移除DOM,动画就没了。所以先让动画播放,播放完再移除。

这个组件有几个亮点:

  • 可配置时长:通过CSS变量传递duration,灵活控制
  • 自动清理:退出时等动画结束再移除DOM,不会出现闪烁
  • 通用性强:任何内容都可以包一层,直接获得淡入淡出效果

我在实际项目中,把这个组件扩展到了列表项、弹窗、提示框等各种场景。你想想看,一个组件写好了,到处都能用,是不是很爽?

避坑指南:如果动画不生效,先检查CSS类名是否拼写正确。我曾经因为把className写成class,排查了半小时...

嗯,这一章的内容就到这里。CSS过渡和动画是React动画的基础,掌握了这些,后面学更复杂的动画库会轻松很多。下一章我们会讲React Transition Group,那个库就是基于我们今天学的这些原理封装的。