---
title: 动效 Motion
url: https://ui.zhaozimin.cn/foundations/motion/
markdown: https://ui.zhaozimin.cn/foundations/motion.md
group: 基础
origin: 设计系统源码（src/system），本仓库是唯一源头
source:
  - src/system/styles/v4/keyframes.css  # https://ui.zhaozimin.cn/src/system/styles/v4/keyframes.css
version: v0.2.0  # 设计系统版本，发布于 2026-09-23
scope: .zzm-v4   # 根节点必须带这个类，否则样式不生效
css: https://ui.zhaozimin.cn/v/0.2.0/zzm.css   # 锁版本地址，本仓库再改也不影响你
---

# 动效 Motion

> 产品内动效 150–250ms，只传递状态。标准缓动 cubic-bezier(.2,.7,.2,1)。戏剧化只留给首次进站的开屏，之后产品永远安静。

## 重播

### 样张 · MotionReplay

React：

```jsx
import { useState } from 'react'
import { Btn } from './components/v4/Button.jsx'

const ACCENT = 'var(--zzm-accent, #A6402F)'
const EASE = 'cubic-bezier(.2,.7,.2,1)'
const MONO = "ui-monospace,'SF Mono',Menlo,monospace"

const ROWS = [
  ['zzmSpring', '1.1s', '弹性落定', { width: 20, height: 20, borderRadius: '50%', background: ACCENT, animation: `zzmSpring 1.1s ${EASE} both` }],
  ['zzmIn', '.35s', '进场上浮', { width: 96, height: 20, borderRadius: 4, background: '#1D1D1B', animation: 'zzmIn .35s ease both' }],
  ['zzmSlide', '.28s', '侧栏滑入', { width: 96, height: 20, borderRadius: 4, background: '#8B8B85', animation: 'zzmSlide .28s ease both' }],
  ['zzmGrow', '.55s', '展开生长', { width: 160, height: 20, borderRadius: 4, background: '#D6D6D1', overflow: 'hidden', animation: `zzmGrow .55s ${EASE} both` }],
  ['zzmPulse', '2.6s ∞', '当前节点呼吸', { width: 20, height: 20, borderRadius: '50%', border: `1.5px solid ${ACCENT}`, animation: 'zzmPulse 2.6s ease-in-out infinite' }],
]

export default function MotionReplay() {
  const [n, setN] = useState(0)
  return (
    <div style={{ display: 'grid', gap: 12, width: '100%' }}>
      {ROWS.map(([name, dur, use, style]) => (
        <div key={name} style={{ display: 'grid', gridTemplateColumns: '96px 56px minmax(0,1fr) 96px', gap: 12, alignItems: 'center', minHeight: 28 }}>
          <span style={{ fontFamily: MONO, fontSize: 11, color: '#161616' }}>{name}</span>
          <span style={{ fontFamily: MONO, fontSize: 11, color: '#A2A29C' }}>{dur}</span>
          <div style={{ minWidth: 0 }}><div key={n} style={style} /></div>
          <span style={{ fontSize: 12, color: '#8B8B85' }}>{use}</span>
        </div>
      ))}
      <div><Btn size="sm" onClick={() => setN(n + 1)}>重播</Btn></div>
    </div>
  )
}
```

HTML（构建时由上面的 React 渲染得到，可直接粘贴）：

```html
<div style="display: grid; gap: 12px; width: 100%">
  <div
    style="
      display: grid;
      grid-template-columns: 96px 56px minmax(0, 1fr) 96px;
      gap: 12px;
      align-items: center;
      min-height: 28px;
    "
  >
    <span
      style="font-family:ui-monospace,&#x27;SF Mono&#x27;,Menlo,monospace;font-size:11px;color:#161616"
    >
      zzmSpring
    </span>
    <span
      style="font-family:ui-monospace,&#x27;SF Mono&#x27;,Menlo,monospace;font-size:11px;color:#A2A29C"
    >
      1.1s
    </span>
    <div style="min-width: 0">
      <div
        style="
          width: 20px;
          height: 20px;
          border-radius: 50%;
          background: var(--zzm-accent, #a6402f);
          animation: zzmSpring 1.1s cubic-bezier(0.2, 0.7, 0.2, 1) both;
        "
      ></div>
    </div>
    <span style="font-size: 12px; color: #8b8b85">弹性落定</span>
  </div>
  <div
    style="
      display: grid;
      grid-template-columns: 96px 56px minmax(0, 1fr) 96px;
      gap: 12px;
      align-items: center;
      min-height: 28px;
    "
  >
    <span
      style="font-family:ui-monospace,&#x27;SF Mono&#x27;,Menlo,monospace;font-size:11px;color:#161616"
    >
      zzmIn
    </span>
    <span
      style="font-family:ui-monospace,&#x27;SF Mono&#x27;,Menlo,monospace;font-size:11px;color:#A2A29C"
    >
      .35s
    </span>
    <div style="min-width: 0">
      <div
        style="
          width: 96px;
          height: 20px;
          border-radius: 4px;
          background: #1d1d1b;
          animation: zzmIn 0.35s ease both;
        "
      ></div>
    </div>
    <span style="font-size: 12px; color: #8b8b85">进场上浮</span>
  </div>
  <div
    style="
      display: grid;
      grid-template-columns: 96px 56px minmax(0, 1fr) 96px;
      gap: 12px;
      align-items: center;
      min-height: 28px;
    "
  >
    <span
      style="font-family:ui-monospace,&#x27;SF Mono&#x27;,Menlo,monospace;font-size:11px;color:#161616"
    >
      zzmSlide
    </span>
    <span
      style="font-family:ui-monospace,&#x27;SF Mono&#x27;,Menlo,monospace;font-size:11px;color:#A2A29C"
    >
      .28s
    </span>
    <div style="min-width: 0">
      <div
        style="
          width: 96px;
          height: 20px;
          border-radius: 4px;
          background: #8b8b85;
          animation: zzmSlide 0.28s ease both;
        "
      ></div>
    </div>
    <span style="font-size: 12px; color: #8b8b85">侧栏滑入</span>
  </div>
  <div
    style="
      display: grid;
      grid-template-columns: 96px 56px minmax(0, 1fr) 96px;
      gap: 12px;
      align-items: center;
      min-height: 28px;
    "
  >
    <span
      style="font-family:ui-monospace,&#x27;SF Mono&#x27;,Menlo,monospace;font-size:11px;color:#161616"
    >
      zzmGrow
    </span>
    <span
      style="font-family:ui-monospace,&#x27;SF Mono&#x27;,Menlo,monospace;font-size:11px;color:#A2A29C"
    >
      .55s
    </span>
    <div style="min-width: 0">
      <div
        style="
          width: 160px;
          height: 20px;
          border-radius: 4px;
          background: #d6d6d1;
          overflow: hidden;
          animation: zzmGrow 0.55s cubic-bezier(0.2, 0.7, 0.2, 1) both;
        "
      ></div>
    </div>
    <span style="font-size: 12px; color: #8b8b85">展开生长</span>
  </div>
  <div
    style="
      display: grid;
      grid-template-columns: 96px 56px minmax(0, 1fr) 96px;
      gap: 12px;
      align-items: center;
      min-height: 28px;
    "
  >
    <span
      style="font-family:ui-monospace,&#x27;SF Mono&#x27;,Menlo,monospace;font-size:11px;color:#161616"
    >
      zzmPulse
    </span>
    <span
      style="font-family:ui-monospace,&#x27;SF Mono&#x27;,Menlo,monospace;font-size:11px;color:#A2A29C"
    >
      2.6s ∞
    </span>
    <div style="min-width: 0">
      <div
        style="
          width: 20px;
          height: 20px;
          border-radius: 50%;
          border: 1.5px solid var(--zzm-accent, #a6402f);
          animation: zzmPulse 2.6s ease-in-out infinite;
        "
      ></div>
    </div>
    <span style="font-size: 12px; color: #8b8b85">当前节点呼吸</span>
  </div>
  <div><button type="button" class="zzm-btn zzm-btn--sm">重播</button></div>
</div>
```

## 缓动与时长

| 用途 | 值 |
|---|---|
| 标准缓动 | `cubic-bezier(.2,.7,.2,1)`——快起慢落 |
| 悬停 / 按压过渡 | `.16s ease`（按钮、卡片、链接） |
| 状态切换 | 150–250ms |
| 进场 | `zzmIn .35s`、`zzmUp .25s` |
| 展开 | `zzmGrow .55s` |
| 弹性强调 | `zzmSpring 1.1s` |

## 关键帧清单

| 名称 | 效果 | 用在哪 |
|---|---|---|
| `zzmPulse` | 透明度 .15 ↔ .55 呼吸 | 学习地图当前节点光晕 |
| `zzmIn` / `zzmIn2` | 上移 12px 淡入 | 屏幕进场 |
| `zzmUp` / `zzmUp2` | 上移 10px 淡入 | 卡片、提示进场 |
| `zzmSlide` | 左移 14px 淡入 | 侧栏滑入 |
| `zzmGrow` | max-height 0 → 960 展开 | 折叠面板 |
| `zzmSpring` | 横移过冲再回弹 | 弹性演示 |
| `zzmFade` | 纯淡入 | 遮罩 |
| `zzmPayIn` | 上移 20px + 缩放 .985 淡入 | 付款页 |
| `zzmShimmer` | 一道微光横扫 | 骨架屏 |
| `zzmBtnSpin` | 旋转一周 | busy 键旋转环 |
| `zzmPillShine` | 快扫一次后长歇 | 档位胶囊反光、busy 键反光 |

全部带 `zzm` 前缀，全局注册无副作用；组件里用内联 `animation:` 引用。`@keyframes zzmDialogIn` 是提炼层为确认窗新增的一条。

## 减少动效

`prefers-reduced-motion: reduce` 时，所有动画与过渡时长压到 0.01ms、只播一次。这段规则在 zzm.css 的基线段里，逐字取自 edu。

```css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
```

## 铁律

- ✓ 要：动效只传递状态：出现、展开、按下、完成
- ✗ 不要：装饰性循环动画（呼吸光晕只给「当前位置」）
- ✓ 要：动 transform 和 opacity
- ✗ 不要：动 width / height / padding 引起重排
- ✓ 要：尊重减少动效设置
- ✗ 不要：开屏式大动画出现在第二次访问

## 本页用到的 CSS

按样张里出现的类名，从源文件原样裁出（完整版见 zzm.css）：

```css
/* tokens.css · 用到的令牌 */
:root {
  --zzm-accent: #A6402F;
}

/* controls.css */

.zzm-v4 .zzm-btn {
  -webkit-appearance: none;
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 36px;
  padding: 0 16px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .2px;
  white-space: nowrap;
  cursor: pointer;
  user-select: none;
  border-radius: 8px;                                   /* 圆角矩形——按钮禁胶囊 */
  border: 1px solid rgba(0, 0, 0, .14);
  color: #3F3F3B;
  background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%);
  box-shadow:
    0 0 0 3px rgba(255, 255, 255, .55),                 /* ④ 反光光晕环 */
    0 1px 2px rgba(0, 0, 0, .09),                       /* ③ 近影 */
    0 2px 6px rgba(0, 0, 0, .06),                       /* ③ 远影 */
    inset 0 1px 0 rgba(255, 255, 255, .92),             /* ② 顶部反光 */
    inset 0 -1px 0 rgba(0, 0, 0, .05);                  /* ① 底缘收沉 */
  transition: box-shadow .16s ease, transform .16s ease, background .16s ease,
    border-color .16s ease, filter .16s ease, opacity .16s ease;
}

.zzm-v4 .zzm-btn:hover {
  transform: translateY(-1px);
  border-color: rgba(0, 0, 0, .18);
  box-shadow:
    0 0 0 4px rgba(255, 255, 255, .8),
    0 2px 4px rgba(0, 0, 0, .10),
    0 4px 12px rgba(0, 0, 0, .09),
    inset 0 1px 0 rgba(255, 255, 255, .95),
    inset 0 -1px 0 rgba(0, 0, 0, .05);
}

.zzm-v4 .zzm-btn:active {
  transform: translateY(0);
  background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%);   /* ⑤ 凹面翻转 */
  box-shadow:
    0 0 0 3px rgba(255, 255, 255, .45),
    0 1px 0 rgba(255, 255, 255, .65),
    inset 0 2px 4px rgba(0, 0, 0, .13),
    inset 0 1px 2px rgba(0, 0, 0, .09);
}

.zzm-v4 .zzm-btn:focus-visible {
  outline: none;
  box-shadow:
    0 0 0 3px rgba(166, 64, 47, .20),                   /* 键盘焦点＝朱色光晕 */
    0 1px 2px rgba(0, 0, 0, .09),
    0 2px 6px rgba(0, 0, 0, .06),
    inset 0 1px 0 rgba(255, 255, 255, .92);
}

.zzm-v4 .zzm-btn:disabled {
  cursor: not-allowed;
  transform: none;
}

.zzm-v4 .zzm-btn:disabled:not(.zzm-btn--busy) {
  color: #A2A29C;
  background: #F2F2EF;
  border-color: rgba(0, 0, 0, .08);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 1px 2px rgba(0, 0, 0, .04);
}

.zzm-v4 .zzm-btn--sm { height: 28px; padding: 0 12px; font-size: 12px; gap: 4px; }

.zzm-v4 .zzm-btn--icon.zzm-btn--sm { width: 28px; }
```

---

上一页：[光照模型](https://ui.zhaozimin.cn/foundations/elevation.md) · 下一页：[版式](https://ui.zhaozimin.cn/foundations/layout.md) · 全站目录：https://ui.zhaozimin.cn/llms.txt
