MOTION · 动效与交互指南

动效是功能,不是装饰

本页讲清楚:页面什么时候需要动效、用什么动效、时长曲线怎么配、以及最容易踩的坑。 每一条规则都配了可见、可点的演示——让动效从"凭感觉"变成"可复制的规则"。

🌿 动效的四大用途

动效不是装饰,而是功能。一个合格的动效,必须能在下面四个用途里找到自己的位置; 找不到的,就该删掉。治愈田园风格的动效整体偏慢、偏柔——像风穿过麦田,而不是霓虹闪烁。

👀

引导注意力

新内容入场、重要变化高亮。让用户的目光被"温柔地牵"到该看的地方,而不是被吓一跳。

🙌

提供反馈

点击有反应、操作有回应。按钮按下的回弹、表单提交的转圈,告诉用户"我收到了"。

🧭

建立空间关系

页面切换、展开折叠的方向感。从哪来、到哪去,用动效的朝向讲清楚,空间就不混乱。

🍃

传递个性

品牌的节奏感:治愈 = 慢、科技 = 快、活泼 = 弹。同样的过渡,曲线不同,气质完全不同。

没有目的的动效就是干扰。 每写一个动效,都先问自己一句:"它在帮用户理解什么?"答不上来,就别加。

⏱️ 时长与曲线速查

动效好不好看,70% 取决于时长曲线对不对。记住这张表, 基本能覆盖 90% 的场景。曲线统一用 cubic-bezier,不要用 ease/linear 这种黑盒值。

场景 时长 曲线 cubic-bezier
微交互
按钮 hover / 按下 / 开关
100–200ms standard(匀感) cubic-bezier(0.4, 0, 0.2, 1)
常规过渡
展开 / 折叠 / 显隐
200–400ms standard cubic-bezier(0.4, 0, 0.2, 1)
入场动效
元素滚入视口
400–800ms decelerate(由快到慢) cubic-bezier(0, 0, 0.2, 1)
页面切换
路由 / 抽屉 / 弹层
300–500ms standard cubic-bezier(0.4, 0, 0.2, 1)
回弹 / 弹性
活泼风格 / 拖拽
300–500ms spring(带过冲) cubic-bezier(0.34, 1.56, 0.64, 1)

这三种曲线,本站的 tokens.css 已经抽成了令牌,直接引用即可:

--motion-easing-standard:   cubic-bezier(0.4, 0, 0.2, 1);   /* 微交互 / 常规 */
--motion-easing-decelerate: cubic-bezier(0, 0, 0.2, 1);    /* 入场:由快到慢 */
--motion-easing-spring:     cubic-bezier(0.34, 1.56, 0.64, 1); /* 弹性回弹 */

实际写起来就一行——别再写 transition: all .3s 这种无曲线的版本了:

.card {
  transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
  /* 或引用令牌:transition: all var(--motion-duration-fast) var(--motion-easing-standard); */
}

🎬 曲线长什么样?让小球跑给你看

三条同样的轨道,小球跑同样的距离——区别只在曲线。点"重播"看三遍,体会 standard(匀)decelerate(冲一下再停)spring(过头再弹回) 的差别。

DEMO · 三种曲线对比
standard匀速感
decelerate由快到慢
spring弹性过冲
注意 spring 的小球会冲过终点再弹回——这就是"过冲",活泼风格才用。

移动端动效宁可短,不要长。 用户在等,超过 600ms 的入场会让人不耐烦。移动端把所有时长按桌面 ×0.7 收一收,体验更利落。

🌱 入场动效

元素滚入视口时,给它一个轻柔的"登场"。最常用、也最不容易出错的组合是 淡入 + 上移:opacity: 0 → 1,translateY: 16px → 0。 本站的 base.css 已经内置了这套(见 [data-reveal]),你只要加属性 + 触发脚本。

规则一:别一次全动,要错峰

一组元素同时入场会很"愣"。让每个元素比前一个晚 50–100ms,形成错峰(stagger), 视觉上就有了呼吸感。下面 5 个色块就是错峰入场——点"重播"再看一次。

DEMO · 错峰入场(stagger 80ms)

规则二:用 IntersectionObserver 触发

不要用 scroll 事件(性能差、抖)。用 IntersectionObserver 监听元素进入视口, 加上 .is-visible 类即可。这也是本站实际用的脚本:

/* 元素带 data-reveal 属性,进入视口加 is-visible 类 */
const io = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add('is-visible');
      io.unobserve(entry.target);   // 只动一次,避免来回滚动重复触发
    }
  });
}, { threshold: 0.15 });              // 露出 15% 才触发

document.querySelectorAll('[data-reveal]').forEach(el => io.observe(el));

对应的 CSS(base.css 已提供,这里展示原理):

[data-reveal] {
  opacity: 0;
  transform: translateY(20px);
  transition:
    opacity 800ms cubic-bezier(0, 0, 0.2, 1),
    transform 800ms cubic-bezier(0, 0, 0.2, 1);
}
[data-reveal].is-visible {
  opacity: 1;
  transform: translateY(0);
}

🎬 实战 demo:滚到这里,它就淡入上来

下面这个卡片就是用上面的方法做的。点"重新播放"会移除可见态,再滚入(或再点一次)即重新触发——这正是真实页面里用户滚动时会看到的效果。

DEMO · 淡入 + 上移(opacity + translateY)
🌾
轻柔地登场
opacity 0→1 · translateY 16→0 · 800ms decelerate

👆 Hover 与微交互

微交互是"手指/鼠标碰到时"的即时回应,时长一律 100–200ms。把鼠标移到下面这些元素上、 按住试试——治愈风格的反馈是克制的:一点点上浮、一点点加深,绝不夸张。

DEMO · Hover 上浮 / 按下回弹
hover 上浮 −2px,按下缩到 0.97,模拟真实按压。

对应的 CSS(.btn:active 的缩放由 components.css 统一提供):

/* hover:轻微上浮 + 阴影增强 */
.btn--lift:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
}
/* active:缩放 0.97,模拟手指按压(已内置) */
.btn:active { transform: scale(0.97); }

卡片 hover:上浮 + 阴影

可点击的卡片,hover 时整体上浮 4px、阴影加深,暗示"我可以点"。这是本站 .card--clickable 的标准行为:

DEMO · 卡片 hover 上浮
🌻

向日葵卡

移上来,我会轻轻抬起。

链接 hover:下划线渐显

正文里的链接,hover 时把淡色下划线变成实色,既不打扰阅读,又给得到反馈:

DEMO · 链接下划线 hover

田园里有一道潺潺的小溪,把鼠标放上去看看。

💧 加载与过渡

等待不可避免,但可以让等待"有结构"。四条原则,各配了演示。

① 骨架屏优于菊花图

空白的转圈让人焦虑;骨架屏用灰块勾勒出"内容长什么样",给用户结构的预期,等待感大幅降低。下面这个会一直 shimmer 闪烁:

DEMO · 骨架屏 shimmer

② 数字递增:大数字从 0 滚到目标

关键数据(销量、用户数)从 0 平滑滚到目标值,比直接显示终值更有冲击力。用 requestAnimationFrame 配 decelerate 曲线即可:

DEMO · 数字递增(1200ms,decelerate)
0人已加入

③ 进度条:平滑过渡

进度条的变化用 transition 平滑过渡,不要跳变。点击按钮,看它如何"流"到 72%:

DEMO · 进度条平滑过渡(1.2s decelerate)

④ 页面切换:淡入淡出 或 滑动

整页/抽屉切换,用淡入淡出(300–500ms)或带方向感的滑动。方向要和操作一致—— "前进"向左滑、"返回"向右滑,空间关系就立住了。

♿ 无障碍与降级

动效是增强,不是必需。对前庭功能敏感、易晕动、或开了"减少动态"的用户, 动效会变成折磨。下面三条是无障碍底线,必须满足。

① 必须尊重 prefers-reduced-motion

用户在系统里开了"减少动态",你的页面就该几乎不动。一行媒体查询兜底所有元素:

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }
}

这是无障碍底线,不是可选项。 关掉所有动效后,页面必须仍然完整可用——内容能读、按钮能点、状态能看清。先做"无动效版",再叠加动效。

🎬 亲手试试:模拟"减少动态"

下面这个开关,会给本页套上和 prefers-reduced-motion 等效的降级样式。打开它,再去上面点那些 demo——你会发现骨架屏不闪了、小球不滚了、数字也不计数了,但页面照样能用。这就是降级该有的样子。

DEMO · 模拟 prefers-reduced-motion
减少动态:关
打开后,回到上面几个 demo 试试——动效几乎冻结,内容依旧清晰。

② 闪烁频率低于 3Hz

任何闪烁/脉冲动画,频率必须 < 3 次/秒(低于 3Hz),以防光敏性癫痫诱发。能不闪就不闪,要闪就极慢。

③ 动效只是锦上添花

不要用动效传递关键信息(比如"红色闪烁 = 报错")。状态变化要有文字/图标/颜色等非动效冗余,关掉动效也能读懂。

🚫 常见错误

下面这些都是反复出现的坑,逐条对照自查:

  • 动效太多。每个元素都在动 = 没有重点。一屏之内,同时进行的动效不超过 2–3 处。
  • 时长过长。入场超过 600ms 就拖沓,移动端尤其明显。能用 400ms 就别用 800ms。
  • 曲线太弹。严肃场景(表单、支付、医疗)用 spring 弹性曲线会显得轻浮。spring 只留给活泼风格和拖拽。
  • 不降级。没处理 prefers-reduced-motion,对敏感用户是生理伤害。一行媒体查询的事,别省。
  • hover 只在桌面有效。移动端没有 hover,光做 hover 效果 = 移动端用户毫无反馈。必须有 :active 或 tap 反馈替代。
  • 用 ease/linear 当万能曲线。这俩是黑盒值,不同浏览器表现不一。统一用 cubic-bezier 写死,行为才可控。
  • 来回滚动反复触发。入场动效触发后要用 io.unobserve() 解绑,否则滚上去再下来会反复闪,既丑又耗电。

一句话总结:动效要慢得有耐心(治愈)、快得有节制(反馈)、降级得有底线(无障碍)。 做到这三点,动效就是加分项;做不到,不如不做。