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。 - 文案点明对象;没有任何状态只靠颜色表达。