Motion
动效
动效不是装饰,而是对操作的回应。瑚琏将缓动曲线与时长收敛成 token,让 CSS 工具类和 motion 动画保持同一种手感。
先问:该不该动
按用户一天看到它多少次决定,而不是按好不好看决定。高频动作上的动画只会让软件显慢。
| 频率 | 典型场景 | 结论 |
|---|---|---|
| 每天 100+ 次 | ⌘K 命令面板、键盘快捷键 | 不做动画 |
| 每天几十次 | hover 态、列表导航 | 去掉或大幅削减 |
| 偶尔 | 对话框、抽屉、Toast | 常规动效 |
| 罕见 / 首次 | 引导、成功庆祝 | 可以有惊喜 |
缓动曲线
悬停轨道即可比较手感。默认 ease-out 被替换为更果断的统一曲线。
减速(默认档)
ease-outcubic-bezier(0.16, 1, 0.3, 1)进场 / 退场 / hover / 按压 —— 起步即最快,用户第一眼就看到位移,因此「感觉」比同时长的其它曲线更跟手。不确定用哪条时用它。
加速后减速
ease-in-outcubic-bezier(0.65, 0, 0.35, 1)元素在屏内位移 / 形变(不涉及出现与消失)。两端都平缓,像真实物体起步与停稳。
抽屉(iOS / Ionic)
--ease-drawercubic-bezier(0.32, 0.72, 0, 1)整屏尺度的滑入滑出:Drawer、ActionSheet。尾段比 ease-out 长得多,大面积位移才不会「冲到位再急停」。
时长
交互动效控制在 300ms 内。180ms 的下拉比 400ms 的下拉感觉更快。
| Token | 时长 | 工具类 | 用于 |
|---|---|---|---|
| fast | 150ms | duration-150 | 按压反馈 / 贴身微交互(100–160ms) |
| base | 200ms | duration-200 | 浮层进出场:Tooltip、Popover、Select、Menu |
| slow | 300ms | duration-300 | 大块转场:Drawer / ActionSheet 面板滑动 |
| entrance | 600ms | duration-600 | 首屏逐级揭示 / 滚动入场(非交互反馈,可从容) |
按压反馈
任何可按下元素都应立即回应,否则界面像没有收到操作。
普通控件按下缩到 0.97,大卡片使用 0.99,避免子元素一起缩放时产生跳动。
// 已经使用 motion 的组件 import { pressable } from "@hulianui/ui"; <m.button whileTap={pressable.whileTap} transition={pressable.transition} /> // 不引入 motion 运行时的纯 CSS 方案 import { pressableClass } from "@hulianui/ui"; <button className={cn("…", pressableClass)} />
浮层从触发器长出
锚定触发器的浮层应从触发器方向展开,而不是从自身中心展开。
/* Base UI 已在 Positioner 上提供原点,直接接入即可 */
<Popup className="origin-[var(--transform-origin)] data-[starting-style]:scale-95 …" />无障碍
减少动效不等于移除所有反馈。
这条媒体查询由库负责
响应 prefers-reduced-motion 是组件自己的事,不是消费方的事。消费方不该为了关掉某个动效去写 !important、去猜库内部的 DOM 结构——那种选择器在结构变动时会静默失效,而失效的表现是「无障碍偏好不生效」,不会有任何报错。发现哪个组件在减弱动效下仍然位移,按 bug 提 issue。
usePrefersReducedMotion() 自绘动效(canvas / rAF / 自己写的内联 style 过渡)从 usePrefersReducedMotion() 取同一个判断,不必再写一份 matchMedia。SSR 与首屏 HTML 恒为 false,hydration 后立即纠正。