Skip to content

Design guidelines 设计规范

用 ranui 组件搭出来的界面,要遵循哪些规则才能读起来像一套系统,而不是一堆零件。

本页讲的是取舍——该用哪个令牌、上线前该检查什么。令牌清单本身在 设计系统,运行时切换与覆盖在主题系统。 这些规则的完整、可机器校验版本在仓库里: packages/ranui/docs/DESIGN.md

适用场景:在用 <r-*> 元素排版页面或搭业务组件,需要决定一个颜色、一段间距、一个字号、 一层投影或一个动效时长时。答案永远是同一句:先确定角色,值交给令牌。

原则

  1. 清晰优先于个性。 主任务和主操作必须一眼可辨,其余才谈得上。
  2. 组合,而不是重造。 先找 r-buttonr-inputr-selectr-modal,再考虑用 div 拼 基础件——组件已经带着焦点、键盘与 ARIA 行为,自己造要重新推一遍。
  3. 只用令牌,不用裸值。 一个 hex、一个 20px 间距、一层手挑的投影,都是不会跟随主题的决定。
  4. 按角色和状态判断,不靠眼睛。「这段文字是什么角色?」(heading / label / copy / button)有 答案;「多大看着舒服?」没有。
  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

不要发明 20px28px。档位有限才有节奏,一处越界就毁掉它。跨区域保持共享的「基线」——对齐的边缘、 基线和栏位——并用渲染出来的像素去核对,而不是靠肉眼。

选排版

先问这段文字是什么角色——heading、label、copy、button、mono——字体、字号、字重、行高就从 排版尺度里跟着定了,不要逐处挑 px。

角色是工具而非法条:真正一次性的装饰文字(播放器手势闪烁提示、激活链接的字重微调)与其硬塞进最接近 的角色,不如给它一个自己的组件令牌。

纵深:投影与层级

按元素「是什么」选投影层级——文档流内的表面、浮层、还是阻塞式对话框——并确认它真的看得见。看不见 的投影等于没做,而回退到卡片层级的浮层看起来像贴在页面上。

在自己的页面骨架里嵌入 ranui 浮层。z-index 阶梯从 1000 起,就是为了越过常规页面骨架, 所以 portal 出去的浮层完全不需要你配合。但留在自己 Shadow DOM 里position: fixed 浮层 (r-modal 的对话框)只能逃到最近的层叠上下文为止——如果你用带 isolationopacity < 1transformfilterwill-change 的容器包住了嵌入内容,那个容器就必须被抬高,对话框才能爬出来。 把抬高限定在浮层真的打开时

css
.embed {
  isolation: isolate; /* 便宜:自己没有 z-index,不会抬高任何东西 */
}
/* 只在真的有浮层打开时抬高——不要「以防万一」 */
.embed:has(r-modal[open]),
.embed:has(r-modal[closing]) {
  position: relative;
  z-index: 100;
}

给容器无条件加 z-index,会把里面所有东西(包括完全静态的内容)在整个滚动周期内抬到你自己的 吸顶导航之上。这个 bug 在本站上真实发生过。记得同时匹配 closingopen 被移除后,遮罩还会按过渡 时长继续绘制。

动效

变化越大,越值得给时间;不够大就别动。悬停与激活反馈约 150ms,菜单约 200ms,对话框约 300ms,本来 就一目了然的变化给 0ms。尊重 prefers-reduced-motion

绝不要让调色属性参与过渡。 CSS 分不清颜色为什么变了,所以给 background-colorcolorborder-colorbox-shadowfillstroke 加的 transition,在主题翻转时同样触发——每个 元素按各自的时长淡变,而页面其余部分早已切换完毕。要动就动运动属性(transformopacity、几何 属性)。transition: alltransition: 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-motionprefers-color-scheme

鼠标与触摸、窄屏与宽屏

输入方式和视口都没有「次要目标」。

  • 拖拽、滑块、手势一律用 Pointer Eventspointerdown / pointermove / pointerup / pointercancel),不要只绑 mouse*,并在真正的拖拽面(而不是更大的容器)上配 touch-action: none。CSS 声明了 touch-action: none 却没有对应的指针处理,是一个坏掉的控件, 不是无害的空操作。
  • 只在悬停时出现的能力必须有点按兜底。 r-select / r-popovertrigger="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
  • [ ] 文案点明对象;没有任何状态只靠颜色表达。

Released under the MIT License.