Design guidelines 设计规范
用 ranui 组件搭出来的界面,要遵循哪些规则才能读起来像一套系统,而不是一堆零件。
本页讲的是取舍——该用哪个令牌、上线前该检查什么。令牌清单本身在 设计系统,运行时切换与覆盖在主题系统。 这些规则的完整、可机器校验版本在仓库里: packages/ranui/docs/DESIGN.md。
适用场景:在用
<r-*>元素排版页面或搭业务组件,需要决定一个颜色、一段间距、一个字号、 一层投影或一个动效时长时。答案永远是同一句:先确定角色,值交给令牌。
原则
- 清晰优先于个性。 主任务和主操作必须一眼可辨,其余才谈得上。
- 组合,而不是重造。 先找
r-button、r-input、r-select、r-modal,再考虑用div拼 基础件——组件已经带着焦点、键盘与 ARIA 行为,自己造要重新推一遍。 - 只用令牌,不用裸值。 一个 hex、一个
20px间距、一层手挑的投影,都是不会跟随主题的决定。 - 按角色和状态判断,不靠眼睛。「这段文字是什么角色?」(heading / label / copy / button)有 答案;「多大看着舒服?」没有。
- 把每个可达状态都设计到。 默认、悬停、激活、聚焦、禁用、加载、空、错误——顺利路径只是八分之一。
- 验证渲染结果。 明色和暗色、窄屏和宽屏、鼠标和触摸。代码评审看不出一层看不见的 投影。
规则冲突时的优先级:用户目标 → 已验证的证据 → 本规范 → 已落地的模式 → 通用经验。
选颜色
颜色按角色与状态分配,不靠眼睛挑。状态阶梯已经替你 决定了悬停和激活长什么样,你要做的是说清角色。
| 这个元素是…… | 用 |
|---|---|
| 页面或表面的背景 | --ran-color-bg / -bg-subtle / -bg-elevated / -bg-muted |
| 指针悬停 / 正在按下 | --ran-color-bg-hover / -bg-active |
| 文字 | --ran-color-text / -text-secondary / -text-disabled |
| 边框 | --ran-color-border / -hover / -active |
| 这个界面存在的那个操作 | --ran-color-primary(其上文字用 --ran-color-primary-text) |
| 状态 | --ran-color-success / -warning / -danger |
| 链接 | --ran-color-link |
每个色彩语义只有一个含义。 主操作是无彩色的——浅色黑底白字、暗色白底黑字——所以别拿蓝色当主色: 蓝色属于链接和聚焦环。绿色是成功、琥珀是警告、红色是危险;拿红色当强调,就是提前花掉一个你以后会 需要的信号。
三条能避免静默失效的规则:
- 该跟随主题的值,绝不写死 hex 或
rgb()。 - 兜底值必须是会翻转的令牌:
var(--ran-color-text, var(--ran-gray-1000)),而不是var(--ran-color-text, #171717)——只在浅色下成立的字面量在暗色里会消失。 - 兜底值引用的令牌必须存在。
var()引用未声明的属性会解析为空,整条声明被丢弃,元素沿用继承 来的值——通常看起来「差不多对」,因此更难发现。(没有--ran-color-error,是--ran-color-danger。)
间距与节奏
所有间距都从九档尺度里取,并让距离本身表达含义:
- 组内元素之间 8px。
- 组与组之间 16px。
- 区块之间 32–40px。
不要发明 20px、28px。档位有限才有节奏,一处越界就毁掉它。跨区域保持共享的「基线」——对齐的边缘、 基线和栏位——并用渲染出来的像素去核对,而不是靠肉眼。
选排版
先问这段文字是什么角色——heading、label、copy、button、mono——字体、字号、字重、行高就从 排版尺度里跟着定了,不要逐处挑 px。
角色是工具而非法条:真正一次性的装饰文字(播放器手势闪烁提示、激活链接的字重微调)与其硬塞进最接近 的角色,不如给它一个自己的组件令牌。
纵深:投影与层级
按元素「是什么」选投影层级——文档流内的表面、浮层、还是阻塞式对话框——并确认它真的看得见。看不见 的投影等于没做,而回退到卡片层级的浮层看起来像贴在页面上。
在自己的页面骨架里嵌入 ranui 浮层。z-index 阶梯从 1000 起,就是为了越过常规页面骨架, 所以 portal 出去的浮层完全不需要你配合。但留在自己 Shadow DOM 里的 position: fixed 浮层 (r-modal 的对话框)只能逃到最近的层叠上下文为止——如果你用带 isolation、opacity < 1、 transform、filter、will-change 的容器包住了嵌入内容,那个容器就必须被抬高,对话框才能爬出来。 把抬高限定在浮层真的打开时:
.embed {
isolation: isolate; /* 便宜:自己没有 z-index,不会抬高任何东西 */
}
/* 只在真的有浮层打开时抬高——不要「以防万一」 */
.embed:has(r-modal[open]),
.embed:has(r-modal[closing]) {
position: relative;
z-index: 100;
}给容器无条件加 z-index,会把里面所有东西(包括完全静态的内容)在整个滚动周期内抬到你自己的 吸顶导航之上。这个 bug 在本站上真实发生过。记得同时匹配 closing:open 被移除后,遮罩还会按过渡 时长继续绘制。
动效
变化越大,越值得给时间;不够大就别动。悬停与激活反馈约 150ms,菜单约 200ms,对话框约 300ms,本来 就一目了然的变化给 0ms。尊重 prefers-reduced-motion。
绝不要让调色属性参与过渡。 CSS 分不清颜色为什么变了,所以给 background-color、color、 border-color、box-shadow、fill、stroke 加的 transition,在主题翻转时同样触发——每个 元素按各自的时长淡变,而页面其余部分早已切换完毕。要动就动运动属性(transform、opacity、几何 属性)。transition: all 和 transition: 0.2s 这类简写等于 all(含调色属性),在 ranui 自身 样式里是禁止的,在你的代码里也不是好主意。
状态与文案
每个可达状态都是设计的一部分:悬停、激活、聚焦、禁用、加载、空、错误。把它们映射到阶梯上—— 悬停 → bg-hover / border-hover;激活 → bg-active;禁用 → text-disabled 加降低不透明度; 聚焦 → 聚焦环。
不可交互的东西不能长得像可交互。r-card 只有加了 hoverable 才响应悬停——不点击的卡片就别加。
文案同样是系统的一部分:
- 按钮要有动作和对象。✅「删除成员」 ❌「删除」「确定」。
- 错误先说发生了什么,再说怎么办。✅「构建失败:产物超过体积上限。请精简产物或调高上限。」 ❌「操作失败,请重试。」
- 确认与提示陈述变化,而不是「成功」。✅「项目已删除」 ❌「删除成功」——提示能弹出来本身就说明 成功了。
- 让上下文消除冗余:标题已经是「删除项目」的对话框,按钮不需要再叫「永久删除该项目」。
无障碍
- 文字与背景的对比度满足 WCAG AA。
- 绝不只用颜色表达状态——配上图标、标签或文字。
- 所有可交互元素都保留可见的聚焦环(
--ran-focus-ring,或outline: 2px solid var(--ran-color-primary); outline-offset: 2px)。不要为了「干净」删掉它。 - 一切都能用键盘到达,不存在只能用鼠标的操作。
- 尊重
prefers-reduced-motion与prefers-color-scheme。
鼠标与触摸、窄屏与宽屏
输入方式和视口都没有「次要目标」。
- 拖拽、滑块、手势一律用 Pointer Events(
pointerdown/pointermove/pointerup/pointercancel),不要只绑mouse*,并在真正的拖拽面(而不是更大的容器)上配touch-action: none。CSS 声明了touch-action: none却没有对应的指针处理,是一个坏掉的控件, 不是无害的空操作。 - 只在悬停时出现的能力必须有点按兜底。
r-select/r-popover的trigger="hover"在触摸 设备上会退化为点击,你自己写的也必须如此。 - 优先用相对视口的尺寸——
%、min()、max()、clamp()、vw/vh,例如min(560px, calc(100vw - 32px))——而不是新造一个断点。ranui 没有共享的断点令牌,所以每个硬断点 都是一个需要有人维护的一次性数字。 - 绝不在移动端隐藏某件事的唯一入口。 用重排代替
display: none。 - 量出来的位置只在下一次重排之前有效。 任何基于
getBoundingClientRect()的结果,都会在窗口 缩放、容器重排、以及(对 portal 出去的面板而言)滚动时失效。要在这些事件上重新测量,而不是只在 最初触发测量的那次交互上。在窄宽度加载页面只验证了初始布局,没有验证从宽拖到窄——而后者 才是这类 bug 真正出现的地方。
组件库机器校验了哪些
其中九条由 pnpm -F ranui verify:design 校验,CI 在每个 PR 上对 ranui 自身源码运行:暗色不安全的 颜色兜底、裸颜色字面量、间距尺度、尺寸尺度、只支持鼠标的拖拽循环、会让 hidden 失效的 :host display 规则、引用未声明令牌的兜底、组件查询自己的 shadow 树、以及在构造函数之外构建 shadow 树。 已知违例被棘轮记录在基线文件里——新增会失败,修好后被悄悄改回去也会失败。
这道闸门覆盖的是组件库而不是你的应用;但它抓的那些失效方式(兜底引用了不存在的令牌、颜色只在浅色 下成立)恰恰是评审时看起来完全正常的那些,所以同样值得用在你自己的 CSS 上。
上线前的检查清单
- [ ] 主任务与主操作一眼可辨。
- [ ] 明色与暗色、窄屏与宽屏下都正常。
- [ ] 鼠标与触摸都可用;任何悬停触发都有点按兜底。
- [ ] 所有状态都验证过——悬停、激活、聚焦、禁用、加载、空、错误。
- [ ] 键盘与焦点验证过;焦点处处可见。
- [ ] 边界情况:长文本、大数字、两种语言。
- [ ] 间距取自尺度、排版按角色、颜色用语义令牌。
- [ ]
transition里没有调色属性;没有transition: all。 - [ ] 文案点明对象;没有任何状态只靠颜色表达。