定位与原则#
- 短。 交互反馈在 0.2 – 0.3 秒内完成。
- 硬。 用位移、擦入、形变和明暗反转;不用弹跳、不用回弹、不用持续漂浮。
- 有因果。 动效解释"什么变了、从哪来":底块从左滑入、文字让位给箭头、遮罩向一侧退出。
- 戏剧性留给入场。 闪烁点亮、加载页这类效果只在进入时出现一次,日常交互保持安静。
- 可以关。 尊重"减少动态效果"的系统设置。
时长与缓动#
图下半的分镜是官网首页通用按钮的悬停(竖条变箭头)。库里的按钮现在照云游戏页的顶栏,悬停只换色,见下文。
| 令牌 | 值 | 用途 | 官网出现次数 |
|---|---|---|---|
--duration-instant |
50ms | 按下的即时反馈 | 1 |
--duration-fast |
200ms | 颜色、透明度变化 | 63 |
--duration-base |
300ms | 位移、展开、形变——默认档 | 119 |
--duration-slow |
400ms | 较大面积的移动 | 8 |
--duration-slower |
600ms | 入场 | 6(0.5s 与 0.6s 合计) |
官网首页的过渡九成集中在 0.2s 和 0.3s 两档(实测)。超过 0.6s 的只有个别的入场与循环动画。(云游戏页:过渡最常见的是 0.15s,14 次;动画最常见的是 0.16s,34 次。实测(云游戏页);这里的时长档不变。)
| 令牌 | 值 | 用途 |
|---|---|---|
ease-standard |
ease |
默认。官网多数过渡没有显式声明缓动,用的就是浏览器默认值 |
ease-emphasis |
ease-in-out |
往返的位移、面板展开 |
ease-exit |
ease-out |
入场、从屏幕外进入 |
ease-linear |
linear |
旋转、跑马灯、闪烁(Tailwind 内置,不在 theme.css 里) |
官网首页的过渡没有使用任何自定义贝塞尔曲线(实测;动画里只有 加载页 铺满的那一层用了一条)。不要引入带过冲的缓动(如 cubic-bezier(0.34, 1.56, 0.64, 1)),它会让界面显得"弹"。
不带过冲的减速曲线也没有加。补完入场之后拿两条和 ease-out 并排比过一次:4 – 8px 的浮层位移看不出差别;大面积的擦入用它们会"先出来一大半、再慢慢收尾",擦的过程反而看不清。所以进场一律是 ease-exit(ease-out),不用再找更"顺"的曲线。(推断)
云游戏页用了几条自定义曲线:cubic-bezier(.5, 0, .5, 1) 13 次,多在"更多"侧栏 80 – 160ms 的换色和淡入淡出上;cubic-bezier(.2, 0, .1, 1) 9 次,是没有定义的 --ease-out-extreme 的回退值(页面里的 --ease-* 变量一个都没定义,生效的都是回退值),标题、标语的上移淡入和大字滑入用它;cubic-bezier(.33, 0, .67, 1) 6 次,"更多"加载层的黄色铺满和移动平台上那套竖排卡片的入场用它。样式表里的 7 种曲线都不带过冲。实测(云游戏页),见 云游戏页实测 7。这里仍不加自定义曲线,缓动令牌还是上表这四个。
在 Tailwind 里:
<button class="transition-colors duration-(--duration-fast) ease-standard">…</button>
<div class="transition-transform duration-(--duration-base) ease-emphasis">…</div>交互反馈的套路#
以下都来自官网实测。
悬停:换色,不上浮#
| 控件 | 悬停时 |
|---|---|
| 深色按钮 | 底 #383838 → #484848,字 #EEEEEE → 白;圆角 2px → 6px |
| 浅色按钮 | 底白 → #F0F0F0 |
| 云游戏页顶栏的导航项 | 深色项底白 20% → 30%,字 #BFBFBF → 白;亮色项("下载")#EEEEEE → 品牌黄;圆角不变(0.15s,实测(云游戏页)) |
| 分页条的翻页钮 | 底 #FAFAFA → 信号黄 |
| 导航图标 | #D9D9D9 → #858585 |
| 页签 | 底透明 → #F3F3F3 |
| 主行动块(黑) | 黄色底层淡入,文字由白变墨 |
| 分享按钮 | 底变墨黑,图标变黄 |
没有任何控件在悬停时放大、上浮或加重阴影。
库里的 按钮 照云游戏页这一行:悬停只换底色和字色,--duration-fast,圆角不跟着变。
悬停:竖条变箭头#
官网首页的深色按钮左侧有一条黄色竖条,在悬停时变成一个向右的三角并右移——"这里可以前往"的暗示。库里的按钮已经不用它(改照云游戏页的顶栏),这里留着它的做法,自己搭"可以前往"的控件时可以用。做法是让同一个伪元素的 clip-path 在两个多边形之间过渡:
.btn::after {
content: "";
position: absolute;
left: 1rem;
top: 22.5%;
width: 0.75rem;
height: 55%;
background: var(--color-action);
clip-path: polygon(0 0, 25% 0, 25% 100%, 0 100%);
transition:
clip-path var(--duration-base) var(--ease-standard),
transform var(--duration-base) var(--ease-standard);
}
@media (hover: hover) {
.btn:hover::after {
clip-path: polygon(0 20%, 100% 50%, 0 80%, 0 80%);
transform: translateX(0.625rem);
}
}两个 polygon 的顶点数必须相同才能插值,所以三角形写成四个点(最后两个重合)。
组件库把这两个形状做成了工具类 marker-bar 与 marker-arrow(见 utilities.css),写法是 marker-bar group-hover:marker-arrow。竖条的宽度用 --marker-bar 调,默认 3px。按钮不再用它们,工具类照旧公开。
激活:让位#
页签被选中时,文字向左平移一小段,右侧淡入一个圆形的箭头。导航轨的项在悬停时,一块灰色底向右展开,文字随之出现。共同点是:新元素出现时,旧元素挪开一点给它腾位置,而不是凭空叠上去。
入场:滑块揭示#
分节标题进入视口时,一个灰色块从左侧滑入(translateX(-100%) → 0),块内的箭头从 −45° 转正,随后标题文字淡入。顺序是"块 → 箭头 → 字",每步错开约 0.1 秒(错开量为推断)。
对应三个工具类:animate-slide-in、animate-turn-in、animate-fade-in,各 300ms、ease-out,错开量用 [animation-delay:100ms] 这样的写法加。
关闭:转 90°#
弹窗的关闭图标在悬停时旋转 90°。这是全站少有的旋转动效。
按下#
按下态只换更深一档的颜色(黄 → #EEEA00,深灰 → #282828),不缩小。云游戏页顶栏的深色导航项按下是白 15%,比平时的 20% 还淡一点——压在深色底上,淡就是暗。
补的几条约定#
上面的套路都来自官网实测。下面这几条官网上没有逐条量到,是把同一套原则用到规范没写到的地方(推断)。逐个控件是怎么补的、过程中定了什么,记在 动效补全计划 里。
状态的记号要有过渡#
勾、圆点、细线、括号这类"状态的记号",出现时不硬切:状态变了,画面要让人看得出是"变过来的"。记号是小件,进出都用 duration-fast。
| 控件 | 什么变了 | 怎么动 |
|---|---|---|
| 复选、单选 | 勾、半选的短横、圆点 | 淡入淡出,200ms,和方格换底色同时走 |
| 输入框、多行文本、验证码输入、分段选择、选择、日期、组合框、标签输入 | 焦点进来 / 离开 | 聚焦的墨线从左往右擦出,300ms;失焦时缩回去,200ms。sunken 是底边线上的一条线,outline 是从左往右露出来的整圈——用裁切,不用缩放(缩放会把四条边压扁、角上变细)。下拉面板开着(选择、日期、组合框)算作聚焦,线留着。错误、禁用、只读时不画:那时边线已经是别的意思 |
| 滑块 | 按住时正中的细线;按键或外部改值后的位置 | 细线淡入淡出;滑块头和走过的一段滑过去,200ms。拖动、点轨道时不过渡 |
| 步骤条 | 走完一步:连接线、节点 | 连接线的墨色从起点一端充到头,300ms;节点换色 200ms |
| 物品格、角括号、文件上传的投放区;图片查看的取景角 | 选中 / 拖到上面时四角的括号;大图层打开时的取景角 | 八段短线各自从角的顶点伸到满长,200ms;消失是直接消失 |
| 验证码输入 | 填进去的那一位 | 字从透明淡入到墨色,200ms;粘贴一整串时各格一起到 |
进出不对称#
进入用 duration-base(300ms),退出用 duration-fast(200ms):来的时候看得清从哪来,走的时候不挡路。记号、文字提示这类小件,进出都用 duration-fast。
方向位移#
浮层从触发它的那一侧来,位移 4px;大面积的(全屏菜单的栏目、横幅)和贴着视口边缘出来的(轻提示)8px。位移只是提示方向,不是滑行——距离再长就成了"飞进来"。
| 令牌 | 值 | 用途 |
|---|---|---|
--motion-shift |
4px | 贴着触发元素出来的浮层:菜单面板、气泡卡片、悬浮卡、文字提示 |
--motion-shift-lg |
8px | 大面积的,和贴着视口边缘出来的:弹窗、全屏菜单的栏目、底部的轻提示、完成横幅的标题 |
- 位移只写在进场的起点上,退场只淡出:走的时候不用再交代一遍"回哪去"。
- 透明度和位移各走各的时长——透明度
duration-fast,位移duration-base、ease-exit,正是时长表里两档各自的用途。面板因此先看得清、再落稳,"进出不对称"也由此而来,不用另写退场的时长。 - 浮层面板的写法收在
lib/motion.ts的enterFromSide里:按基元给的data-side(面板在触发元素的哪一侧)决定起点往哪边偏。
| 浮层 | 从哪来 | 怎么动 |
|---|---|---|
| 菜单面板一族:下拉菜单与子菜单、右键菜单、下拉选择、组合框、日期选择、气泡卡片、悬浮卡、侧轨收起时的二级 | 触发元素那一侧,4px;子菜单从它那一行的侧面来 | 淡入 200ms,位移 300ms,和顶上那条强调条同一拍;退场只淡出 200ms |
| 弹窗、确认弹窗 | 下面,8px | 遮罩淡入 200ms;面板淡入 200ms,位移 300ms。退场只淡出 200ms。不放大 |
| 轻提示(底部) | 下面,8px | 淡入 200ms,位移 300ms;退场只淡出。正中的那一种是官网实测的做法,只有透明度 |
| 全屏菜单 | 整层原地淡入;栏目从左,8px | 整层 200ms;栏目每条 300ms,错开。退场整层淡出 200ms |
| 图片查看的大图层 | 整层原地淡入 | 整层 200ms;随后取景角落位 200ms;大图取到了淡入 200ms。退场整层淡出 200ms |
| 文字提示 | 触发元素那一侧,4px | 小件:淡入和位移都是 200ms;退场只淡出。键盘聚焦、从相邻的提示移过来时立刻出现,不过渡 |
云游戏页的弹窗是另一种走法:下载弹窗、商城都是遮罩淡入、面板从右边 40px(translateX(2.5rem))淡入,0.16s,退场反向,160ms 后卸载;通用确认框也从右 40px 来。用户中心的下拉菜单从上往下滑入 8px,0.2s、ease-out。页里的账号 SDK 弹窗只淡入淡出,0.3s,宽于 600px 时不缩放、不位移,窄于 600px 才是从下方滑上来的抽屉。以上是实测(云游戏页)与实测(账号 SDK),见 云游戏页实测 8.10、8.11、9.2、9.4 和第 11 节。这里不变:弹窗从下面 8px 来,菜单一族从触发元素那一侧 4px 来。
错开#
一组东西逐项入场时,每项错开 50 – 100ms,最多错开前五六项,后面的一起到。整组从第一项开始动到最后一项停下,不超过 600ms。
- 用
animate-shift-in(见 标志性动画)加[animation-delay:…];第几项用nth-*变体写在每一项自己身上,不用在外面数。 - 全屏菜单 的栏目:从左 8px;第一项等 100ms(整层的淡入过半),之后每项晚 50ms,第六项起一起到(350ms)。从第一项动到最后一项停下 550ms。
- 错开只用在"整屏换掉"这样的入场上。列表、时间线、步骤条不做成默认的逐项入场。
入场#
只播一次的入场,留给"这一块刚出现"的时候。分节标题的那一种是官网实测的(见上面的"入场:滑块揭示");下面这些是照它的节奏补的。
| 控件 | 怎么动 | 多久 | 什么时候播 |
|---|---|---|---|
| 完成横幅 | 整块从左擦入;标题那一组从左 8px 淡入归位,右边的行动淡入,都比色带晚 200ms | 色带 300ms;从开始到停下 500ms | 进视口 |
| 方括号标题 | 两个括号淡入,名称晚 100ms 淡入。不位移:行内的字做不了 | 各 300ms;一共 400ms | 进视口 |
| 小型面积图 | 整张图从左擦出来,面积和折线一样。同一屏的几张一起画,不逐张错开 | 600ms | 进视口 |
| 空状态 | 整块淡入,不位移:只是为了不"啪"地一下出现 | 300ms | 挂上 |
| 滚动数字 | 数字从 0 滚到终值,先快后慢;宽度由终值占着。值后来变了直接换,不再滚 | 600ms | 进视口 |
| 页头 | 微文字行 → 标题 → 说明,各自从左 8px 淡入归位,依次晚 100ms。面包屑、返回、行动区不动 | 各 300ms;一共 500ms | 挂上;吸顶的不播 |
- 进视口才播的用
useInView:没进视口时停在起点(裁掉,或者透明),进了播一次,滚走再回来不重播。 - 挂上就播的是纯 CSS 的动画:给那些挂上的那一刻就在眼前的东西。
- 都带一个
animate属性,默认开;关掉之后没有等待态,也不播。 - 能点的东西(按钮、链接)不做位移入场,最多跟着所在的那一块淡入:位置在动的东西不好点。
- 一组入场从第一样开始动到最后一样停下,不超过 600ms(同 错开)。
展开与增删#
内容多出来、少下去的时候不硬切:多出来的把位置撑开或者淡入,少下去的先收起、再从页面里拿掉。(推断)
- 退场后再卸载。 要走的东西先留在页面里把退场走完,再卸载。共用的是内部钩子
usePresence(见 hooks):它等的是这个元素身上正在跑的过渡和动画,不另抄一份时长;一个都没有就当场卸载——测试环境里没有动效,东西撤掉就是撤掉了。加载页 的滑出用的就是它。 - 状态先变,画面后到。
aria-expanded、aria-describedby、aria-invalid这些在状态变的那一刻就改,不等退场走完;退场中的东西点不到、Tab不进去,对读屏是隐藏的。 - 一开始就在的不播。 载入时就展开着的、就带着错误的、就选中的,都是静止的;后来才出现的才播。
| 控件 | 什么变了 | 怎么动 | 多久 |
|---|---|---|---|
| 页签 | 换了一块面板 | 新的那一块淡入,不位移;旧的直接换掉 | 200ms |
| 表格的行展开 | 多出一行明细,或者明细收回去 | 明细区按高度长出来、收回去,只动高度不淡;几行一起开时同时、同速 | 展开 300ms,收起 200ms |
| 字段 的错误说明 | 出错了,或者改对了 | 按高度长出来并淡入,撤掉时反过来;下面的内容被推着走 | 小件:进出都是 200ms |
| 提示条 | 关掉了(open 置为假) |
高度收到 0 并淡出,连它占的间距一起收;后面的内容跟着补上来。只做了关闭 | 200ms |
| 标签输入、文件上传 的列表 | 加了一项 | 新的那一项淡入;一次加几个一起到,不错开。删掉的见 内容切换与补位 | 200ms |
- 按高度收放不量高度。 外面一层是只有一行的网格,行高在
0fr和1fr之间过渡,里面一层裁掉还放不下的那一截(lib/motion.ts的collapse)。展开的起点写在@starting-style里:展开到一半又收起,过渡从半路折回去。不支持它的旧浏览器只是没有展开的过程,收起照常。 - 提示条是量着收的。 它不能多包一层(
className是给带边线的那一层的),所以收起时量一次自己的高度,把高度、上下的内边距和边线、外边距一起过渡到 0;父容器纵向排着、带行间距时,下外边距收到负的行间距。过渡仍然是 CSS 的,"减少动态效果"照常管得到。
选中指示的滑动#
几项里选一项的控件,选中的那块底、那段粗条不是"在这一项上消失、在那一项上出现",而是同一块东西滑过去:换的是位置,东西没换。(推断)
| 控件 | 指示是什么 | 怎么动 |
|---|---|---|
| 分段选择 | 选中那一段的墨色底 | 沿轨道滑到新的一段;各段等宽,只走位置 |
| 页内目录 | 引线上那一段 3px 的粗条 | 沿引线滑到新的一项,长度跟着那一项的高度变。一项都不亮时原地淡出,再亮时原地淡入 |
| 页签 的胶囊变体 | 选中那一个的墨色底 | 滑到新的那一个,宽度跟着变;盖在路过的胶囊的轮廓线上面。页签栏横向滚动时跟着内容走 |
- 200ms,
ease-standard,和各项换字色同一拍——时长表里"位移"那一档是 300ms,两个并排比过:300ms 时字先反过来、墨块晚 100ms 才到,定的是 200ms。状态(aria-checked、aria-selected、aria-current)和字色在选中的那一刻就变,不等它滑到。 - 云游戏页上量到了同一种做法。 顶栏"首页 / 更多"下面那根和字等宽的白线连同小三角,换页签时整组平移,0.2s、
ease;"更多"侧栏的当前项是一块#E6E6E6的底块,换项时整块滑过去,0.16s 匀速,同时一根品牌黄的下划线用 0.24s 从左长出来;初次载入时的当前项不播动画。实测(云游戏页),见 云游戏页、云游戏页实测 8.1、8.8。这一节原是推断,现在有了官方页面上的实测例子;规则和 200ms 都不变。 - 只有"换了一项"才滑。 第一次到位、同一项挪了地方或变了大小(容器宽了、字体到了、前面多了一项)都直接到位:那不是选中变了。中途又换了一项,过渡从半路改道,不排队。
- 从没有到有是原地出现。 一项都没选中时指示淡出,留在原地;再有的时候位置直接到、原地淡入,不从上一个位置滑来。
- 量位置的是内部钩子
useIndicator(见 hooks):量出当前项相对容器的位置和尺寸,写成容器上的四个变量。指示画在容器的::after上(lib/motion.ts的indicator),不多一个元素;位置走translate,宽和高只在各项大小不一时才变。 - 量不到就照旧。 服务端渲染、还没排版、容器藏着的时候,各项照原来的样子自己画选中态;量到了才换成滑动的那一块。任何时候都不会有一帧没有选中态。
- 官网实测的那几种不改:格子式页签的 让位、楔形页签。
内容切换与补位#
眼前的内容换了一批、少了一项、挪了地方的时候,让人看得出"是从哪换来的、往哪去了"。(推断)
| 控件 | 什么变了 | 怎么动 | 多久 |
|---|---|---|---|
| 页内目录 | 点了一项,页面跳到那一节 | 平滑滚过去。目录里的粗条直接滑到点的那一项,途中不一项一项闪过去 | 浏览器定的 |
| 月历(日期选择、日期范围的面板里也是它) | 翻到了另一个月 | 新的一个月从翻的那一侧来 8px 并淡入:往后翻从右,往前翻从左。旧的直接换掉;月份标题、星期那一行不动 | 300ms |
| 标签输入 | 删了一个小块 | 被删的原地淡出;同时后面的小块和输入框从旧位置滑到新位置 | 200ms |
| 文件上传 的列表 | 移除了一行 | 那一行按高度收起并淡出,连行距一起收;下面的行和控件后面的内容被带着上来 | 200ms |
主题(useTheme) |
亮暗换了 | 整页交叉淡化一次:旧的样子淡出,新的淡入。写死主题的局部(data-theme 在某一块上)新旧一个样,看不出变化 |
200ms |
- 平滑滚动是借来的。 页内目录点一项仍然是浏览器的锚点跳转——地址、后退、
:target、scroll-margin、Tab的起点都是原生的那一套。控件只在点的那一下给滚动容器和<html>写上内联的scroll-behavior: smooth,这一次滚动停下就还原成原来的值。系统开了"减少动态效果"时不写,直接跳。 - 连着来的不重播。 月历连着翻(按住
PageDown、连点翻月钮)时,上一次的还没播完,这一次就不播、直接换;停下来再翻才播。每次都从透明起的话,格子会一直闪。动效不拦输入:翻月、移焦点都是当场生效的。 - 补位是先记位置、后算差、再过渡。 内部钩子
useFlip(见 hooks)每次渲染后记下一排里各项的位置;有一项走了的那一次,把其余挪了地方的先用translate摆回旧位置(不过渡),再撤掉——撤的那一下由各项自己的过渡滑回原位(--duration-fast)。只动translate,不碰布局。走的那一项脱离排版、钉在原处把退场走完,别的项从它上面滑过去(lib/motion.ts的flipBox、flipLeaving)。正滑到一半又少一项,从眼前的位置接着走。 - 容器矮了,高度跟着收。 少了一行,那一排就矮了:当场缩上去的话,还在滑的那几项会露在外面,后面的内容也跟着跳一下。所以高度是量着收的(和 提示条 同一个办法)——先撑在眼前的高度上,再过渡到新的,收完把写上去的撤掉;这期间那一排把竖向超出去的裁掉。
- 只有"少了一项"才补位。 加一项、容器宽了、字体到了,各项都是直接到位的。一排超过 50 项、量不到位置(服务端渲染、还没排版、藏着)时不做。
- 走的项先留着。 列表里被拿掉的项不当场消失,带着
data-leaving留在原来的位置上把退场走完,再从页面里拿掉——内部钩子useLingering,等退场用的是 退场后再卸载 的那一套。状态先变:它在被拿掉的那一刻就对键盘和读屏隐藏,焦点的去处、表单的值、计数看到的始终只有还在的那几项。这一次变化里同时有加进来的(整批换掉、"只收一个"换了一个)就不留,照旧直接换。 - 横着排、会换行的用补位;竖着的一列用收起。 标签输入的小块是补位:谁挪到哪一行说不准,只能量。文件列表是一列,被移除的那一行按高度收起(展开与增删 的收放)——用补位的话,列表变矮的那一下后面的内容会直接跳上来一行。
- 主题是整页一起换的。 不走视图过渡的话,带换色过渡的元素是 200ms 过去的,别的是硬切,参差不齐。
useTheme的setTheme和"跟随系统"变了的那一下,亮暗真的变了、浏览器支持视图过渡时,在视图过渡里换;时长挂在<html>上一个只在切换期间才有的标记下(data-theme-switching),使用方自己的视图过渡不受影响。不支持的浏览器照旧直接切;挂载时把记着的主题同步到<html>的那一次不算切换,不淡。 - 换内容不重挂。 月历的格子在翻月时是同一批元素(焦点就在里面),动画是在日期那一块上标一下方向、撤了再标来重播的(
animate-shift-in,见 标志性动画)。
跟手的不加过渡#
拖动、滑动手势进行中,位置必须贴着指针:这时候加过渡,东西会追着手指跑。过渡只给"跳过去"的那种变化——按键、点一下、外部改值。
标志性动画#
| 工具类 | 内容 | 用途 | 来源 |
|---|---|---|---|
animate-flicker |
三次短闪后点亮,600ms | 大图、媒体、标题块入场 | 关键帧节奏:实测;时长:推断 |
animate-fill |
scaleX(0) → scaleX(1),600ms |
进度条、色块充填(需设 transform-origin: left) |
实测 |
animate-scroll-hint |
下坠 1.5rem 并淡出,循环 | 滚动提示 | 实测 |
animate-breathe |
缩放 1 → 1.2 → 1,循环 | 提示点、等待态 | 实测 |
animate-marquee |
平移 −50% 后停顿,循环 | 巨字跑马灯、过长的单行文字 | 实测 |
animate-spin |
匀速旋转,1 秒一圈 | 加载指示 | 实测 |
animate-spin-slow |
同一个关键帧,60 秒一圈 | 刻度圆环 | 推断(官网只量到"匀速旋转一周",没有量到时长) |
animate-blink |
硬切的明灭 | 输入光标、录制指示 | 观察 |
animate-slide-in |
translateX(-100%) → 0,300ms |
分节标题的滑块(外层要 overflow: hidden) |
实测;时长为推断 |
animate-turn-in |
rotate(-45deg) → 0,300ms |
滑块里的斜箭头 | 实测;时长为推断 |
animate-fade-in |
不透明度 0 → 1,300ms | 分节标题的文字;方括号标题、完成横幅的行动、空状态;页签切过来的面板、新加的标签和文件(它们用 200ms) | 实测;时长为推断 |
animate-indeterminate |
平移自身宽度的 150% 再返回,匀速,1.2s 一程,循环 | 不确定进度的色块(色块宽为轨道的 40%) | 推断 |
animate-indeterminate-y |
同上,竖着走 | 竖着的轨道上的不确定进度:开屏页贴左缘的那根条 | 推断 |
animate-pulse |
不透明度 1 → 0.6 → 1,2s,循环 | 骨架的明度往返 | 推断 |
animate-shift-in |
从偏着的地方淡入归位,300ms。往哪边偏由用的地方给:[--shift-x:…] / [--shift-y:…],量用 --motion-shift 那两档;不给就只是淡入 |
一组东西 错开 入场:全屏菜单的栏目;入场:完成横幅的标题、页头的三行字;月历翻月 | 推断 |
animate-wipe-in |
clip-path 从左往右露出来,300ms。有东西画到盒子外面一点的,用 [--wipe-bleed:…] 把另外几边让出来 |
入场:完成横幅的色带、小型面积图(它用 600ms) | 推断 |
animate-bracket-in |
八段短线各自从角的顶点伸到满长,200ms | 角括号 出现。画在 ::after 上,所以写成 after:animate-bracket-in |
推断 |
闪烁点亮#
不透明度的时间线是 0 → 0.5 → 0 → 0.5 → 0 → 0.5 → 0 → 1,三次闪烁分别落在 10%、20%、40%。前两次挨得近、第三次隔得远,模拟设备通电时的不稳定。
- 只用于入场,播放一次。
- 同一视口同时只让一个元素闪烁。
- 不要用在文字段落和可交互控件上。
跑马灯#
内容复制一份首尾相接,匀速移动到 −50% 后停顿一下再循环(关键帧在 80% 到位,80% – 100% 静止)。这个停顿让它看起来像机械装置,而不是无休止的滚动。
已实现为 <Marquee duration gap pauseOnHover paused overflowOnly>:
- 复制那一份由组件来做,副本对读屏隐藏、不可聚焦;两份之间的间隔用
gap调,默认 2rem。 - 一程默认 20 秒,用
duration调:内容越长给得越大,速度才不会变快。 - 要能停下来。 鼠标悬停或焦点落到里面时暂停(默认开);键盘和触屏用户靠
paused——在旁边放一个"暂停滚动"的按钮。 overflowOnly:内容放得下就是一行普通的文字,放不下才动起来,容器宽度变了会重新判断。用在"可能过长的单行文字"上。- 系统开启"减少动态效果"时不动、不显示副本,内容静止在起点,放不下的部分被裁掉。
加载页#
官网首页首次载入时的那一页:近黑底(#141414),一根贴左缘的黄色进度条,大号的百分比跟着条的末端走,画面偏右是标志和一行标语。百分比的数字与 % 用两种字号。
2026-10-14 实测到的完整流程是三个阶段(数值见 measurements.md 8.13):
| 阶段 | 做什么 | 时间 |
|---|---|---|
| 充填 | 进度每变一次,条长、读数的位置、读数的数字、底图的模糊一起补间 | 每次 0.5 秒 |
| 铺满 | 全部就绪后等条走满,黄色从左往右盖过整屏;先慢后快,cubic-bezier(1, 0, 0.7, 1) |
等 0.5 秒,走 0.6 秒 |
| 淡出 | 停一下,整层的不透明度降到 0,露出页面;页面自己的入场这时才起 | 第 1.4 秒起,走 1 秒 |
从就绪到完全消失一共 2.4 秒。这是全库里唯一"为了播完动画而等"的地方,也只在整页的首次载入出现一次。
云游戏页有两种加载。首次进入是 #171717 底加背景图,中间一个灰色标志做 2s 的呼吸(不透明度 1 → 0.4 → 1),没有进度,一直显示到登录状态确认完。每次进"更多"盖的那一层和官网首页的这一页是同一个设计——左缘品牌黄 #FFF100 的竖向进度条、Updating...、标语 OVER THE FRONTIER / INTO THE FRONT——只是时长、曲线和标志不同:走满后黄色从左铺满,1s、cubic-bezier(.33, 0, .67, 1);铺满之后整层淡出,1s,再卸载;每次从首页切到"更多"都重放。实测(云游戏页),见 云游戏页、云游戏页实测 8.13。开屏页仍照官网首页这三段走。
库里有两个组件:
<SplashScreen>(开屏页)照这三段走,排法也是官网的。铺满用的就是animate-fill的关键帧,换成上面那条曲线;进度是根上两个注册过的变量(--ef-splash-progress、--ef-splash-percent)在过渡,条、读数、模糊都由它们算,所以天然同拍。页面到淡出开始的那一刻才放开。详见 反馈。<Loader>(加载页)是简化版:数字和标语在左下角、底部一根横条,就绪后整块向上滑出,600ms(--duration-slower)、ease-emphasis,一开始退场就放开页面,不为了播完动画而拖延。它是先前只照样式表里的色值和字号做的,排法和退场是当时的推断,和官网不一样;留着是因为"只要一块盖住整页的等待"时它更轻。详见 反馈 的"加载"。
两者都在加载期间锁定输入;系统开了"减少动态效果"时都不等,直接撤掉。
滚动#
- 官网首页是整屏切换的长滚动,每个版块进入时触发各自的入场动画。(云游戏页的"更多"是一条普通的纵向长页,不劫持滚轮,每节露出来时用
IntersectionObserver播一次入场,不重播。实测(云游戏页),见 云游戏页。) - 组件库不提供滚动劫持。入场动画用
IntersectionObserver触发,播放一次,不随滚动反复重播。 - 不做视差。巨字可以用跑马灯横向移动,但不跟滚动联动。
减少动态效果#
theme.css 末尾已包含全局降级:系统开启"减少动态效果"时,所有动画与过渡的时长压到接近 0、延迟归零,循环动画只播一次。延迟不归零的话,错开的入场会变成一项一项蹦出来——比动着进来更扎眼。
那条规则只管得到 CSS 的动画和过渡。脚本驱动的动效要自己看:一帧一帧自己算的(滚动数字)用 useReducedMotion,开了就直接给终值;脚本里要的平滑滚动(回到顶部、页内目录点一项)同理。视图过渡的那几层也不归它管:useTheme 在系统开了"减少动态效果"时不走视图过渡,直接切。
云游戏页是一个现成的例子:样式表里有 18 条 prefers-reduced-motion 规则,"更多"视图的入场(闪烁、擦出、上移、大字滑入)压到 0.01ms、延迟归零、只播一次,停在终态;侧栏和顶栏指示的滑动、加载层的过渡直接关掉;脚本里点目录的平滑滚动也换成直接跳。实测(云游戏页),见 云游戏页、云游戏页实测 7。和这里的做法一致。
写组件时仍要自查:
- 降级后内容必须停在终态(
animation-fill-mode: both),不能停在透明或屏幕外。 - 状态变化不能只靠动效表达:页签选中除了位移还有底色变化。
- 跑马灯降级后要保证文字可读(静止在起点,溢出部分可省略)。
宜 / 忌#
宜
- 默认用
duration-base+ease-standard,不确定时不要调。 - 用
transform和opacity做动画。 - 让新元素的出现伴随旧元素的让位。
- 给
:hover套@media (hover: hover),避免触屏上粘住悬停态。Tailwind v4 的hover:变体默认就是这样,组件里直接用它。
忌
- 弹跳、回弹、果冻式的缓动。
- 悬停时放大卡片、上浮加阴影。
- 元素持续漂浮、呼吸、发光(提示点除外)。
- 每张卡片都带入场动画,或每次滚动都重播。
- 用动效拖延用户:超过 0.6 秒的过渡要有充分理由。
- 给跟手的位置加过渡:拖动中的滑块、被手指带着走的抽屉和轻提示。
来源#
- 时长统计、关键帧、各控件的悬停变化:measurements.md 第 7、8 节。
- 加载的三阶段流程:LaoBiDeng321/Endfield-Style-Skill。
- 云游戏页的时长与缓动统计、页签与侧栏的滑动、弹窗进场、两种加载、减少动态效果:measurements-cloud.md 第 7、8、9、11 节(2026-10-24 实测);概览见 云游戏页。
- 令牌定义:theme.css。