本页目录

Design guidelines 设计规范

先明确元素的用途和交互状态,再选择对应的设计令牌(token),让颜色、间距和排版保持一致。

设计系统提供令牌,主题系统说明运行时覆盖, 信息架构负责页面结构。本页是统一维护的 DESIGN.md 的摘要。

需要其他方面的规则时,从界面规范索引进入:输入、焦点和反馈见 INTERACTION,组件实现见 CODING。只有涉及队列、账号连接等任务时,才需参考可选工作流模式。安装后也可从 node_modules/ranui/docs/ 读取这些文档。

适用场景:为 <r-*> 界面选择颜色、间距、排版、纵深或动效。

原则

  1. 清晰优先。 让用户一眼看懂页面的主要任务,以及下一步该做什么。
  2. 复用已有控件。 优先使用原生控件,或满足需求的 r-button、r-input、r-select、r-modal。保留它们已有的表单、键盘和无障碍行为,避免仅为外观而重新实现。
  3. 只用令牌,不用裸值。 一个 hex、一个 20px 间距、一层手挑的投影,都是不会跟随主题的决定。
  4. 先确定用途,再选择样式。 先判断是标题、标签、正文还是按钮,再从对应的排版令牌中选择;避免每处都单独试字号和字重。
  5. 把每个可达状态都设计到。 默认、悬停、激活、聚焦、禁用、加载、空、错误:默认态只是八个状态里的一个。
  6. 在浏览器中验证。 检查浅色与暗色、窄屏与宽屏、鼠标与触摸。只读代码无法确认投影是否可见,也无法确认控件是否容易操作。

规则冲突时的优先级:用户目标 → 已验证的证据 → 本规范 → 已落地的模式 → 通用经验。

选颜色

先确定颜色用于背景、文字、操作还是状态,再选择对应的语义令牌。 状态阶梯提供悬停和按下状态的取值。

这个元素是…… 用
页面或表面的背景 --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
让嵌套圆角协调确实需要内外两层边界时,内圆角等于外圆角减去内缩距离:16 − 4 = 12。这样两条圆弧共享圆心,边缘间距更均匀。 16 − 4 = 12

让小图标更容易点击

图标本身是 16px,点击区域扩展到 32px,让操作更容易点中。实际尺寸应根据鼠标或触摸等输入方式选择,并让相邻点击区域保持间隔。

--ran-size-1 · --ran-size-7
让小图标更容易点击图标本身是 16px,点击区域扩展到 32px,让操作更容易点中。实际尺寸应根据鼠标或触摸等输入方式选择,并让相邻点击区域保持间隔。 16px 32px

上线前的检查清单

按受影响的页面和变更行为选择检查;仓库 CI 仍运行完整门禁。

  • 主任务与主操作一眼可辨。
  • 浅色与暗色、窄屏与宽屏下都正常。
  • 鼠标与触摸都可用;任何悬停触发都有点按兜底。
  • 受影响的状态都验证过:悬停、激活、聚焦、禁用、加载、空、错误。
  • 键盘与焦点验证过;焦点处处可见。
  • 边界情况:长文本、大数字、应用支持的语言。
  • 间距取自尺度、排版按角色、颜色用语义令牌。
  • transition 里没有调色属性;没有 transition: all。
  • 文案与上下文共同明确操作及对象;没有任何状态只靠颜色表达。