Design System: 点击原点色块转场
一种用于站内页面切换的连续转场:用户点击链接或按钮后,纯色色块从点击位置扩张并覆盖整个视口;新页面准备好后,色块再向同一位置收缩成一点,露出新内容。
参考交互:Project Simply。本文提取的是可复用的运动语言与实现规则,不绑定原网站的品牌、页面结构或固定配色。
1. 核心体验
该转场把一次页面跳转表现为一个完整的视觉闭环:
点击 → 色点出现 → 圆形色块扩张 → 全屏遮挡 → 页面切换
→ 新页面位于色块下方 → 圆形色块收缩 → 回到点击点 → 转场结束
- 起点明确: 动画从用户实际点击的位置发生。
- 遮挡完整: 页面切换发生在色块完全覆盖视口之后。
- 空间连续: 新页面中的收缩阶段沿用同一颜色、圆心比例和尺寸逻辑。
- 节奏利落: 扩张比收缩更快,产生“进入迅速、揭示从容”的感觉。
- 内容安全: 页面加载和 DOM 变化隐藏在全屏色块之后。
这不是普通淡入淡出,也不是一张矩形遮罩横向擦过页面。主角是一个从交互点产生、覆盖视口后又回到该点的圆。
2. 视觉结构
转场只需要一个固定覆盖层和一个圆形伪元素:
<div class="page-transition" aria-hidden="true"></div>
覆盖层
- 固定覆盖整个视口:
position: fixed; inset: 0。 - 位于所有页面内容上方。
- 推荐
z-index: 9999或项目统一的最高转场层级。 - 默认透明且不接收指针事件。
- 不添加文字、加载图标、进度条或纹理。
圆形色块
- 使用覆盖层的
::before创建。 position: absolute。border-radius: 50%。- 圆心与用户点击坐标重合。
- 初始状态为
scale(0)。 - 完全覆盖状态为
scale(1)。 - 圆的实际直径在转场开始前动态计算,确保覆盖最远的屏幕角。
3. 点击原点
坐标来源
正常鼠标或触控点击时:
originX = event.clientX;
originY = event.clientY;
点击位置必须基于视口坐标,而不是页面滚动坐标。CSS 变量:
--transition-origin-x: 50vw;
--transition-origin-y: 50vh;
键盘与程序触发
如果链接通过键盘、辅助技术或程序触发,缺少可靠指针坐标,则回退到视口中心:
originX = viewportWidth / 2
originY = viewportHeight / 2
不要使用按钮 DOM 元素的左上角作为原点。若希望键盘触发仍与按钮建立空间关系,可以使用按钮中心点作为次级回退。
跨页保持
必须保存点击点相对于视口的比例,而不是只保存像素值:
xRatio = originX / viewportWidth;
yRatio = originY / viewportHeight;
新页面使用当前视口尺寸恢复圆心:
originX = xRatio * newViewportWidth;
originY = yRatio * newViewportHeight;
这样即使跳转期间发生窗口尺寸、浏览器栏或移动端视口变化,收缩终点仍保持合理。
4. 覆盖半径
圆必须覆盖从点击点到视口四个角中最远的一个。
const farthestX = Math.max(originX, viewportWidth - originX);
const farthestY = Math.max(originY, viewportHeight - originY);
const radius = Math.hypot(farthestX, farthestY) + safetyMargin;
safetyMargin推荐2–4px,防止亚像素边缘漏出。- 圆形直径为
radius * 2。 - 不使用固定的
100vw或100vh,因为点击点靠近边缘时可能无法覆盖对角。 - 可使用
80vmax作为无脚本回退,但动态计算更可靠。
对应 CSS:
.page-transition::before {
width: calc(var(--transition-radius, 80vmax) * 2);
height: calc(var(--transition-radius, 80vmax) * 2);
left: var(--transition-origin-x, 50vw);
top: var(--transition-origin-y, 50vh);
transform: translate3d(-50%, -50%, 0) scale(0);
}
5. 动画阶段
Phase 0:静止
- 覆盖层
opacity: 0。 - 圆形色块
scale(0)。 pointer-events: none。- 页面正常可操作。
Phase 1:扩张覆盖
用户点击有效站内链接后:
- 阻止浏览器立即跳转。
- 记录目标 URL、颜色和点击原点。
- 计算覆盖半径。
- 显示覆盖层。
- 圆从
scale(0)动画到scale(1)。 - 等待扩张动画结束。
- 执行页面导航。
推荐时长:
--transition-out-duration: 350ms;
推荐缓动:
cubic-bezier(0.42, 0, 0.58, 1) /* ease-in-out */
扩张必须完整结束后才能跳转。提前导航会让旧页面闪退,延迟太久则会让全屏色块产生停顿感。
Phase 2:全屏保持
- 页面加载期间圆保持
scale(1)。 - 整个视口只显示纯色色块。
- 保持阶段没有固定动画时长,由页面加载速度决定。
- 不在此阶段显示 spinner,除非页面经常需要超过约
1.2s才能响应。
Phase 3:收缩揭示
新页面初始化时:
- 读取上一次保存的转场状态。
- 恢复颜色、圆心比例与覆盖半径。
- 首帧强制圆处于
scale(1)。 - 浏览器完成一次样式计算后,切换到收缩状态。
- 圆从
scale(1)动画到scale(0)。 - 动画结束后清理状态与 CSS 变量。
推荐时长:
--transition-in-duration: 550ms;
收缩阶段比扩张阶段慢约 1.4–1.7 倍。它承担新页面的揭示过程,需要比覆盖阶段更从容。
6. 状态模型
推荐使用四个明确状态:
| 状态 | 覆盖层 | 圆形 scale | 用途 |
|---|---|---|---|
idle |
隐藏 | 0 |
页面正常状态 |
out |
显示 | 0 → 1 |
覆盖旧页面 |
in-start |
显示 | 1 |
新页面首帧 |
in-active |
显示 | 1 → 0 |
揭示新页面 |
CSS 示例:
.page-transition {
position: fixed;
inset: 0;
z-index: 9999;
opacity: 0;
pointer-events: none;
}
.page-transition::before {
content: "";
position: absolute;
left: var(--transition-origin-x, 50vw);
top: var(--transition-origin-y, 50vh);
width: calc(var(--transition-radius, 80vmax) * 2);
height: calc(var(--transition-radius, 80vmax) * 2);
border-radius: 50%;
background: var(--transition-color);
transform: translate3d(-50%, -50%, 0) scale(0);
transform-origin: 50% 50%;
transition:
transform var(--transition-out-duration, 350ms)
ease-in-out;
will-change: transform;
}
.page-transition.is-out {
opacity: 1;
}
.page-transition.is-out::before {
transform: translate3d(-50%, -50%, 0) scale(1);
}
.page-transition.is-in-start {
opacity: 1;
}
.page-transition.is-in-start::before {
transform: translate3d(-50%, -50%, 0) scale(1);
transition: none;
}
.page-transition.is-in-active {
opacity: 1;
}
.page-transition.is-in-active::before {
transform: translate3d(-50%, -50%, 0) scale(0);
transition:
transform var(--transition-in-duration, 550ms)
ease-in-out;
}
in-start 到 in-active 之间必须强制浏览器提交一帧,可读取 offsetWidth 或使用双层 requestAnimationFrame。否则浏览器可能合并两次样式更新,导致收缩动画不执行。
7. 颜色系统
转场颜色应来自目标页面、目标内容或当前品牌主题,而不是随机生成。
推荐接口:
<a href="/project" data-transition-color="#4ACFED">View project</a>
触发时读取:
const color =
link.dataset.transitionColor ||
getComputedStyle(document.documentElement)
.getPropertyValue("--default-transition-color");
配色关系
- 色块必须完全不透明,避免页面切换时发生重影。
- 颜色应与新页面建立联系,可使用目标页面的主题色或主视觉辅助色。
- 颜色需要在全屏显示
350–900ms时依然舒适。 - 避免纯白色闪屏和纯黑色硬切。
- 避免高频随机切换多种饱和色。
- 同一内容分类可以共用一种转场色,帮助建立导航记忆。
- 若链接没有专属颜色,使用统一品牌强调色。
参考页面使用过的颜色包括:
- 紫色
#754AF0 - 蓝色
#4ACFED - 粉色
#FFB5D6 - 绿色
#ABF7A1 - 黄色
#E4D515
这些值仅用于说明“链接可携带目标色”的机制,不要求其他项目照搬。
8. 导航状态传递
传统多页网站可通过 sessionStorage 保存一次性状态:
sessionStorage.setItem("transition-pending", "1");
sessionStorage.setItem("transition-color", color);
sessionStorage.setItem("transition-x-ratio", String(xRatio));
sessionStorage.setItem("transition-y-ratio", String(yRatio));
新页面读取后必须立即删除:
const pending =
sessionStorage.getItem("transition-pending") === "1";
sessionStorage.removeItem("transition-pending");
sessionStorage.removeItem("transition-color");
sessionStorage.removeItem("transition-x-ratio");
sessionStorage.removeItem("transition-y-ratio");
SPA 或客户端路由框架不需要 sessionStorage,可以在转场控制器中保留状态:
await cover()
await router.navigate(target)
await nextPageReady()
await reveal()
禁止把转场状态长期写入 localStorage,否则刷新或未来访问时可能错误触发。
9. 链接拦截规则
只拦截符合以下条件的普通站内导航:
- 同源链接。
- 鼠标主键点击。
- 没有按住
Ctrl、Cmd、Shift或Alt。 target为空或为_self。- 目标 URL 与当前 URL 不同。
- 链接未声明
data-no-transition。
以下情况直接使用浏览器默认行为:
- 外部网站。
mailto:、tel:、javascript:。- 锚点
#section。 - 下载链接。
- 新标签页链接。
- 修饰键点击。
- 浏览器前进与后退,除非项目专门实现对应转场。
转场开始后设置全局锁,防止快速重复点击触发多个导航。
10. 响应式
- 使用
clientX/clientY和当前视口尺寸,不依赖桌面端固定分辨率。 - 移动端保存圆心比例,适应地址栏伸缩和方向变化。
- 横竖屏都通过最远角公式计算半径。
- 不把转场圆写死为
200vw,超宽屏或点击点偏移时可能不足或浪费性能。 - 在小屏设备上可以将扩张时长缩短到
280–320ms,收缩保持450–550ms。
11. 性能规范
- 只动画
transform,不动画width、height、top或left。 - 圆形尺寸在动画开始前一次性计算。
- 使用
translate3d和will-change: transform。 - 页面内只保留一个全局转场元素。
- 转场结束后移除状态类和临时 CSS 变量。
- 不在圆形色块上使用模糊、阴影、噪点或复杂渐变。
- 避免在动画期间触发同步布局读取与写入循环。
12. 可访问性与降级
减少动态效果
@media (prefers-reduced-motion: reduce) {
.page-transition::before {
transition-duration: 1ms !important;
}
}
减少动态效果时仍可保留瞬时色块遮挡,但不执行明显的圆形缩放。
导航可靠性
- 动画失败时仍必须完成导航。
- 设置导航兜底计时器,不能依赖
transitionend作为唯一跳转条件。 - JavaScript 未运行时,普通
<a href>必须仍然可用。 - 色块仅为装饰:
aria-hidden="true"。 - 转场层不获取焦点,也不向屏幕阅读器播报无意义状态。
- 新页面显示后,焦点管理遵循项目本身的路由规范。
13. 禁用模式
- 不从固定屏幕中心启动所有转场,除非没有点击坐标。
- 不用矩形色块冒充该效果。
- 不在色块上放 Logo、Loading 文案或百分比。
- 不使用弹性回弹;覆盖边缘会短暂露出旧页面。
- 不让色块半透明。
- 不在圆扩张结束前跳转。
- 不在新页面已露出后继续长时间保留覆盖层。
- 不同时使用页面缩放、内容旋转和强烈模糊。
- 不对外链、下载、邮箱和新标签页导航强制转场。
- 不因动画逻辑破坏浏览器默认的修饰键点击行为。
14. 验收标准
- 点击任意有效站内链接后,圆从实际点击点产生。
- 圆在约
350ms内完整覆盖四个视口角。 - 页面切换期间看不到旧页面闪退、白屏或新页面半成品。
- 新页面中的圆从全屏状态向原点击点收缩。
- 收缩阶段约
550ms,视觉上比扩张阶段更从容。 - 不同目标链接可以携带不同转场色。
- 快速双击不会触发两次导航。
- 外链、新标签页和修饰键点击仍遵循浏览器默认行为。
- 窗口尺寸改变后圆仍能完整覆盖视口。
- 降低动态效果模式下可以安全、快速地完成导航。