Design guidelines 设计规范
先明确元素的用途和交互状态,再选择对应的设计令牌(token),让颜色、间距和排版保持一致。
设计系统提供令牌,主题系统说明运行时覆盖, 信息架构负责页面结构。本页是统一维护的 DESIGN.md 的摘要。
需要其他方面的规则时,从界面规范索引进入:输入、焦点和反馈见 INTERACTION,组件实现见 CODING。只有涉及队列、账号连接等任务时,才需参考可选工作流模式。安装后也可从 node_modules/ranui/docs/ 读取这些文档。
适用场景:为
<r-*>界面选择颜色、间距、排版、纵深或动效。
原则
- 清晰优先。 让用户一眼看懂页面的主要任务,以及下一步该做什么。
- 复用已有控件。 优先使用原生控件,或满足需求的
r-button、r-input、r-select、r-modal。保留它们已有的表单、键盘和无障碍行为,避免仅为外观而重新实现。 - 只用令牌,不用裸值。 一个 hex、一个
20px间距、一层手挑的投影,都是不会跟随主题的决定。 - 先确定用途,再选择样式。 先判断是标题、标签、正文还是按钮,再从对应的排版令牌中选择;避免每处都单独试字号和字重。
- 把每个可达状态都设计到。 默认、悬停、激活、聚焦、禁用、加载、空、错误:默认态只是八个状态里的一个。
- 在浏览器中验证。 检查浅色与暗色、窄屏与宽屏、鼠标与触摸。只读代码无法确认投影是否可见,也无法确认控件是否容易操作。
规则冲突时的优先级:用户目标 → 已验证的证据 → 本规范 → 已落地的模式 → 通用经验。
选颜色
先确定颜色用于背景、文字、操作还是状态,再选择对应的语义令牌。 状态阶梯提供悬停和按下状态的取值。
| 这个元素是…… | 用 |
|---|---|
| 页面或表面的背景 | --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,都是可选参考,而非必须等待的时长。动效应能被新操作打断,并尊重 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;禁用 → 禁用语义令牌;仅在需要且仍清晰可读时降低不透明度;聚焦 → 聚焦环。
不可点击的内容不应显示点击反馈。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 树。已知的违例记录在基线文件里并单向收紧:新增会失败,修好后再被悄悄改回去也会失败。
这些检查只扫描命令配置的源码范围,不会自动检查使用 ranui 的外部应用。本仓库的文档站等页面另有对应检查。应用开发者仍需使用自己项目的检查命令,并在浏览器中确认布局、主题和交互是否正确。
设计图解
基于 ranui 基础 token 绘制的示意图,并非真实组件。
保持内容对齐
某一行增加内容时,文字仍与其他行左对齐,右侧操作也保持在同一列。
让嵌套圆角协调
确实需要内外两层边界时,内圆角等于外圆角减去内缩距离:16 − 4 = 12。这样两条圆弧共享圆心,边缘间距更均匀。
--ran-radius-lg − --ran-space-1 = --ran-radius-md让小图标更容易点击
图标本身是 16px,点击区域扩展到 32px,让操作更容易点中。实际尺寸应根据鼠标或触摸等输入方式选择,并让相邻点击区域保持间隔。
--ran-size-1 · --ran-size-7上线前的检查清单
按受影响的页面和变更行为选择检查;仓库 CI 仍运行完整门禁。
- 主任务与主操作一眼可辨。
- 浅色与暗色、窄屏与宽屏下都正常。
- 鼠标与触摸都可用;任何悬停触发都有点按兜底。
- 受影响的状态都验证过:悬停、激活、聚焦、禁用、加载、空、错误。
- 键盘与焦点验证过;焦点处处可见。
- 边界情况:长文本、大数字、应用支持的语言。
- 间距取自尺度、排版按角色、颜色用语义令牌。
-
transition里没有调色属性;没有transition: all。 - 文案与上下文共同明确操作及对象;没有任何状态只靠颜色表达。