基于原子 CSS 的图标动效方案
- 2026.05.31
一、背景
团队在升级平台框架时,为侧边栏图标增加了 Hover 动效。框架覆盖约 200 个云产品,共有 2000 多个图标。
如果逐个使用 After Effects 制作,成本较高,也难以保证一致性。
因此,我们需要一套可以批量生成、统一约束并支持人工调整的方案。AI 可以参与生成,但它需要建立在稳定的动效系统之上。
二、技术选型
首先比较三种常见的图标动效方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| GIF | 兼容性好,所见即所得 | 文件大、分辨率固定、深色模式难以适配、不能交互控制 |
| Lottie | 动画丰富,支持复杂路径 | 需要 JSON 文件 + lottie-web(~60KB),与 CSS 体系割裂,存在渲染开销 |
| CSS | 矢量无损、体积小、无 JS 运行时、与 Tailwind 生态融合 | 复杂动画的表达能力有限 |
侧边栏图标通常只需要位移、缩放、旋转和少量 3D 变换,CSS 足以覆盖大部分效果。Lottie 需要为图标加载 JSON 和运行时,应用到 2000 个图标时成本较高;GIF 也难以适配深色模式。因此,最终选择 CSS。
三、CSS 原子动效
3.1 原子化
选择 CSS 后,数量问题仍然存在。为 2000 个图标逐个编写 Keyframes 和 Animation,并不会减少工作量。
Tailwind CSS 提供了一个可用的模型:把动效拆成较小的单元,再由设计师组合。界面可以用 flex、p-4、text-sm 构建,动效也可以用 scale-75、rotate-12 一类原子类描述。
这种方式也适合 AI。与从零生成 Keyframes 相比,从有限的动效单元中选择和组合更稳定,也更容易控制输出的一致性。
基于这个方向,我采用了 Tailwind v3 的原子动效库 tailwindcss-motion 作为起点。
tailwindcss-motion 已经定义了较完整的动效分类和预设值。但它基于 Tailwind v3 的 JS 插件架构(matchUtilities + addBase),不能直接用于 Tailwind v4 的纯 CSS 体系。我使用 Claude Code 将其改写为 Tailwind v4 的 @utility 与 CSS Variable 实现,并针对 SVG 图标做了裁剪和扩展。
新增:
- 描边(draw):tailwindcss-motion 没有的动画类型。通过
stroke-dashoffset实现线条描绘生长,要求 SVG 元素设置pathLength="1"。为此扩展了 3 个槽位(draw-in/out/loop)。
描边动画中,
stroke-dasharray设为1.1,而不是精确的1;stroke-dashoffset同时设置0.05的初始偏移。当 Dasharray 等于路径长度时,浏览器在边界值附近的亚像素渲染可能产生轻微闪烁。让路径略微超出边界,可以避开这个临界值。
- 摇晃(shake):在 keyframes 中内置多级衰减振荡(振幅按 -0.5、0.25、-0.1、0 逐级衰减),复用 rotate 槽位,不增加新的 CSS 变量。
- 反向动画:支持负号前缀语法,如
-motion-translate-x-in-50(反向平移)、-motion-draw-in-100(从路径终点反向描绘)。对 AI 生成和人工手写都更直观。 - 触发机制:tailwindcss-motion 的动画在元素挂载时自动播放。本方案增加了
data-motion-trigger属性系统,支持 hover 和 click 触发,未触发时通过animation-name: none !important完全抑制动画,不依赖 JS 事件。 - SVG transform-origin 修复:SVG 元素默认
transform-origin: 0 0(左上角),HTML 元素默认50% 50%(中心)。通过[data-motion-icon]作用域自动设置transform-box: fill-box,确保旋转和缩放围绕元素自身中心。
移除:
- Preset 预设系统:tailwindcss-motion 有 30+ 个预组合的动画预设(
motion-preset-fade、motion-preset-slide等)。在本方案中这个职责交给了 AI——由 AI 根据图标语义从原子零件库组合方案,而不是人工从预设中选择。 - Filter、text-color、background-color 动画:图标动效场景中用不到,直接移除,减少槽位数量和 CSS 体积。
- Loop 默认从 infinite 改为 1:控制台图标的循环动效通常只需有限次数(如齿轮旋转一两圈后停止),无限循环需要显式添加
motion-loop-infinite。
调整:
- 默认时长从 700ms 降到 300ms,更适合微交互节奏
- 补充了基于
linear()的弹簧缓动(spring-smooth/snappy/bouncy/bouncier/bounciest),来自 kvin.me/css-springs - 所有
@keyframes统一包裹在@media (prefers-reduced-motion: no-preference)中(tailwindcss-motion 只包裹了 transform 类,color/opacity 类未包裹)
tailwindcss-motion 是一个通用 Web 动效库。本项目保留了它的槽位架构,并针对 SVG 图标场景进行调整。
项目支持以下动效:
| 动效 | 入场 | 出场 | 循环 | 说明 |
|---|---|---|---|---|
| 位移 | 从上下左右飞入 | 向上下左右飞出 | 上下左右浮动 | 支持反向移动 |
| 缩放 | 从小到大出现 | 从大到小消失 | 心跳、呼吸效果 | 支持单轴缩放 |
| 旋转 | 旋转进入 | 旋转退出 | 持续旋转 | 支持反向旋转 |
| 摇晃 | 晃入 | 晃出 | 持续晃动 | 阻尼振荡效果 |
| 淡入淡出 | 渐显 | 渐隐 | 闪烁 | — |
| 描边生长 | 线条描绘进入 | 线条擦除 | 线条呼吸 | SVG 路径动画 |
系统包含 22 种缓动曲线,覆盖标准 Easing、回弹、弹簧和弹跳效果。默认时长为 300ms,并支持配置延迟、循环次数和变换原点。
3.2 tailwindcss-motion 的动画槽位方案
tailwindcss-motion 的槽位设计解决了多个动画工具类相互覆盖的问题。
给一个元素写两个动画,原生 CSS 可以轻松实现:
.element {
animation:
scale-in 0.3s,
rotate-in 0.3s;
}
但通过工具类组合时(animate-scale-in animate-rotate-in),后一个 animation 声明会覆盖前一个,而不是追加。
进场动画与循环动画的组合更复杂。一个元素可能先从左侧进入(in),再在原地持续微动(loop)。前者只执行一次,后者持续执行。由于 animation 属性会被整体替换,两个独立类名不能直接完成这种组合。
3.3 动画槽位
它的做法是预先声明所有槽位。每个工具类只向对应槽位写入动画值,其余槽位保持 none。本质上,这是用 CSS Variable 的 Fallback 机制实现多路复用:
| 阶段 | 槽位 | 属性 |
|---|---|---|
| enter(进场) | 1–6 | scale-in, translate-in, rotate-in, opacity-in, bg-color-in, draw-in |
| exit(出场) | 7–12 | scale-out, translate-out, rotate-out, opacity-out, bg-color-out, draw-out |
| loop(循环) | 13–18 | scale-loop, translate-loop, rotate-loop, opacity-loop, bg-color-loop, draw-loop |
6 属性 × 3 阶段 = 18 个槽位。概念如下:
@utility motion-translate-x-in-* {
--motion-translate-in-animation: /* 实际的动画值 */;
animation:
/* slot 1-6 (enter) */
var(--motion-scale-in-animation),
/* → none */ var(--motion-translate-in-animation); /* → 实际动画 */
/* ... 其余 16 个槽位同理 */
}
实际代码(Tailwind v4 @utility):
@utility motion-translate-x-in-* {
--motion-origin-translate-x: --value(--motion-translate-*, [percentage], [length]);
--motion-translate-in-animation: motion-translate-in
calc(
var(--motion-translate-duration, var(--motion-duration)) *
var(--motion-perceptual-duration-multiplier)
)
var(--motion-translate-timing, var(--motion-timing))
var(--motion-translate-delay, var(--motion-delay)) both;
animation:
var(--motion-scale-in-animation), var(--motion-translate-in-animation),
var(--motion-rotate-in-animation), var(--motion-opacity-in-animation),
var(--motion-background-color-in-animation), var(--motion-draw-in-animation),
var(--motion-scale-out-animation), var(--motion-translate-out-animation),
var(--motion-rotate-out-animation), var(--motion-opacity-out-animation),
var(--motion-background-color-out-animation), var(--motion-draw-out-animation),
var(--motion-scale-loop-animation), var(--motion-translate-loop-animation),
var(--motion-rotate-loop-animation), var(--motion-opacity-loop-animation),
var(--motion-background-color-loop-animation), var(--motion-draw-loop-animation);
}
所有工具类共享同一份 18 槽 animation 声明。组合 motion-scale-in-75 motion-translate-x-loop-25 时,每个类只写入自己的槽位,因此不会相互覆盖。
Loop 槽位使用 animation-composition: accumulate,使循环动画叠加在进场动画的最终状态上。元素可以先完成进场,再从当前位置继续循环。
3.4 任意值
预设值不够时,可以用方括号语法指定任意 CSS 值:
<g class="motion-translate-x-in-[12px] motion-rotate-loop-[30deg]">
<path d="..." pathLength="1" />
</g>
Tailwind v4 的 --value() 从类名里提取值,直接映射到 CSS 变量。
3.5 无障碍
所有 Keyframes 都包裹在 @media (prefers-reduced-motion: no-preference) 中。用户在系统中减少动态效果后,动画不会执行。
3.6 触发方式
侧边栏动效主要由 Hover 触发。触发逻辑尽量保留在 CSS 中,通过 data-motion-trigger 属性和 CSS 属性选择器控制动画状态:
[data-motion-trigger="hover"]:hover {
--motion-trigger: running;
}
将 motion-trigger 放在外部容器上,Hover 侧边栏的 List Item 时,即可触发内部图标。
在 React 中,motion-react 提供一个简单的 Hook:
import { useHoverMotionTrigger } from "motion-react/hooks";
function MyIcon() {
const hoverProps = useHoverMotionTrigger<HTMLDivElement>();
return (
<div {...hoverProps}>
<SettingsIcon />
</div>
);
}
useHoverMotionTrigger 返回一个 ref 和 data-motion-trigger="hover" 属性,其余部分由 CSS 处理。它不监听 mouseenter 或 mouseleave,也不需要切换 State 或编写 JS 动画逻辑。
动画的播放与暂停由浏览器的 CSS 引擎处理,不会触发 React 重渲染,也不需要 JS 计算进度。对于包含 2000 多个图标的系统,这可以减少运行时成本。
四、项目结构
项目采用 Monorepo,包含三个部分:
css-motion-system/
├── apps/studio/ # Next.js Web 应用
├── packages/
│ ├── motion/ # 纯 CSS 动效库
│ └── motion-react/ # React 图标组件
依赖关系为 studio → motion-react → motion,单向无循环。
motion 不需要构建,CSS 可以直接发布。motion-react 通过 tsup 打包 ESM、CJS 和类型定义。studio 是供设计师制作图标动效的本地 Web 应用。
五、Web UI 工作流

Studio 将图标动效流程拆成四步。AI 负责生成初始结果,设计师负责判断和调整。
Step 1:上传 SVG。通过拖拽导入文件。
Step 2:SVGO 优化与语义分组。SVGO 负责移除冗余节点、统一属性顺序,并为 Path 添加 pathLength="1"。随后使用 Gemini 3.1 Flash 分析 SVG 源码和渲染图,生成分组建议。例如,齿轮图标可以被识别为“外围齿牙”和“中心圆孔”两个语义组:
<svg viewBox="0 0 24 24">
<g data-group="outer teeth">
<path d="..." />
</g>
<g data-group="center hole">
<circle ... />
</g>
</svg>
编辑器支持叠图比较,用不同颜色显示分组,也允许手动调整结构。
Step 3:生成动效方案。设计师可以选择动画类型、时长、缓动曲线和循环次数,并实时预览。

AI 也可以根据图标语义生成三套方案。以齿轮图标为例:
- 方案 A:外齿牙慢转(loop),中心孔淡入(in)
- 方案 B:整图标从左边滑入(translate-in),旋转 90°(rotate-in)
- 方案 C:描边生长(draw-in),配上轻微缩放弹跳(scale-in + spring-bouncy)
选定方案后,仍可以调整动画类型、时长、缓动曲线和循环次数。
Step 4:导出。支持两种格式:
- React TSX 组件,带类型,
size/strokeWidthprops - 独立 SVG,CSS 内嵌,非 React 场景也能用
TSX 可以直接写入 motion-react 的源码目录,构建后发布到 npm。
六、Skill
除 Web UI 外,完整流程也被封装为 Skill。用户可以通过自然语言完成 SVG 输入、SVGO 优化、AI 分组、方案生成、选择与调整、TSX 生成和组件写入。
在终端中输入 SVG 代码后,也可以使用 Sub-agents 并行处理多个图标。
Skill 适合批量生成,GUI 适合调整单个图标,两种工作流可以配合使用。
七、总结
这套方案包含四个部分:
- 基于
tailwindcss-motion的槽位架构,为 SVG 图标补充和调整原子动效; motion-react库,提供 React 图标组件与 Hook;- Studio Web UI,支持 AI 生成和人工调整;
- Skill,通过自然语言完成相同流程。
目前,这套系统已用于公司官网和部分产品侧边栏。熟练设计师可以在一天内完成约 15 个侧边栏图标的动效,实际制作效率约为原流程的 20 倍。
八、致谢
感谢 romboHQ 的 tailwindcss-motion,以及 Tailwind CSS 提供的原子化思路。