Design system 设计系统
ranui 构建于其上的设计语言,以及表达这套语言的完整令牌清单——组件库声明的每一个全局 --ran-* 自定义属性,都附上它在明暗两套主题下的取值。组件消费这些令牌而非写死数值,因此覆盖一个 令牌就能重塑所有消费它的地方。
三个页面回答三个不同的问题,刻意拆开:
| 页面 | 回答 |
|---|---|
| 设计系统(本页) | 令牌是什么——词汇表 |
| 设计规范 | 做界面时如何取舍 |
| 主题系统 | 运行时如何切换与覆盖 |
适用场景:需要查某个令牌的名字或取值(颜色角色、间距档位、图标尺寸、投影层级、缓动曲线), 或想理解这些阶梯为什么长这样。
设计语言:Geist
ranui 的令牌基于 Geist——Vercel 的开源设计体系。它的核心思想是 颜色是一条「状态阶梯」,而不是调色板:色阶不是一组好看的深浅供人挑选,而是一组职责。200 不是「稍深一点的灰」,它就是「悬停背景」。阶梯一旦固定,交互状态就不再需要临场判断。
ranui 把这套阶梯落成 --ran-* 色阶,在其上叠加语义令牌,并以 Geist Sans / Geist Mono 作为 默认字体。
两层令牌
第一层——基础色板:下面这些原始色阶,很少直接消费。
第二层——语义令牌:--ran-color-* 等,映射到第一层。请消费这一层。 暗色模式只重定义第一 层,每个语义令牌都通过 var() 自动翻转,整个组件库不存在任何组件级的暗色覆盖。
--ran-gray-1000 → #171717(浅色) / #ededed(暗色) ← 第一层,会翻转
--ran-color-text → var(--ran-gray-1000) ← 第二层,跟随
--ran-btn-color → var(--ran-color-text, …) ← 组件令牌这条链条就是整个架构:改基础档位则全局传导,改语义令牌则改一个角色,改组件令牌则只改一个元素。
颜色
状态阶梯
每条色阶从 100 走到 1000,每一档职责固定:
| 档位 | 职责 | 档位 | 职责 |
|---|---|---|---|
| 100 | 默认背景 | 600 | 激活边框 |
| 200 | 悬停背景 | 700 | 实心填充(按钮/徽标) |
| 300 | 激活(按下)背景 | 800 | 实心填充——悬停 |
| 400 | 默认边框 | 900 | 次要文字与图标 |
| 500 | 悬停边框 | 1000 | 主要文字与图标 |
背景
| 令牌 | 浅色 | 暗色 | 用于 |
|---|---|---|---|
--ran-background-100 | #ffffff | #000000 | 页面背景 |
--ran-background-200 | #fafafa | #000000 | 轻微区分区域 |
灰阶 —— --ran-gray-100..1000
文字、边框与表面背后的色阶。
| 档位 | 浅色 | 暗色 |
|---|---|---|
| 100 | #f2f2f2 | #1a1a1a |
| 200 | #ebebeb | #1f1f1f |
| 300 | #e6e6e6 | #292929 |
| 400 | #eaeaea | #2e2e2e |
| 500 | #c9c9c9 | #454545 |
| 600 | #a8a8a8 | #878787 |
| 700 | #8f8f8f | #8f8f8f |
| 800 | #7d7d7d | #7d7d7d |
| 900 | #4d4d4d | #a0a0a0 |
| 1000 | #171717 | #ededed |
半透明灰 —— --ran-gray-alpha-100..1000
半透明,可以叠在任意表面上——遮罩、悬停蒙层,或者必须压在未知内容之上的分隔线,都该用它。
| 档位 | 浅色 | 暗色 |
|---|---|---|
| 100 | #0000000d | #ffffff12 |
| 200 | #00000015 | #ffffff17 |
| 300 | #0000001a | #ffffff21 |
| 400 | #00000014 | #ffffff24 |
| 500 | #00000036 | #ffffff3d |
| 600 | #0000003d | #ffffff82 |
| 700 | #00000070 | #ffffff8a |
| 800 | #00000082 | #ffffff78 |
| 900 | #000000b3 | #ffffff9c |
| 1000 | #000000e8 | #ffffffeb |
蓝 —— --ran-blue-100..1000
只保留给链接与聚焦环。
| 档位 | 浅色 | 暗色 |
|---|---|---|
| 100 | #f0f7ff | #06193a |
| 200 | #e9f4ff | #022248 |
| 300 | #dfefff | #002f62 |
| 400 | #cae7ff | #003674 |
| 500 | #94ccff | #00418b |
| 600 | #48aeff | #0090ff |
| 700 | #006bff | #006efe |
| 800 | #0059ec | #005be7 |
| 900 | #005ff2 | #47a8ff |
| 1000 | #002359 | #eaf6ff |
红 —— --ran-red-100..1000
危险与错误。
| 档位 | 浅色 | 暗色 |
|---|---|---|
| 100 | #ffeeef | #330a11 |
| 200 | #ffe8ea | #440d13 |
| 300 | #ffe3e4 | #5d0e17 |
| 400 | #ffd7d6 | #6f101b |
| 500 | #ffb1b3 | #88151f |
| 600 | #ff676d | #f32e40 |
| 700 | #fc0035 | #f13242 |
| 800 | #ea001d | #e2162a |
| 900 | #d8001b | #ff565f |
| 1000 | #47000c | #ffe9ed |
琥珀 —— --ran-amber-100..1000
警告。
| 档位 | 浅色 | 暗色 |
|---|---|---|
| 100 | #fff6de | #2a1700 |
| 200 | #fff4cf | #361900 |
| 300 | #fff1c1 | #502800 |
| 400 | #ffdc73 | #5b3000 |
| 500 | #ffc543 | #703e00 |
| 600 | #ffa600 | #ed9a00 |
| 700 | #ffae00 | #ffae00 |
| 800 | #ff9300 | #ff9300 |
| 900 | #aa4d00 | #ff9300 |
| 1000 | #561900 | #fff3d5 |
绿 —— --ran-green-100..1000
成功。
| 档位 | 浅色 | 暗色 |
|---|---|---|
| 100 | #ecfdec | #002608 |
| 200 | #e5fce7 | #00320b |
| 300 | #d3fad1 | #003a0e |
| 400 | #b9f5bc | #004615 |
| 500 | #82eb8d | #006717 |
| 600 | #4ce15e | #00952d |
| 700 | #28a948 | #00ac3a |
| 800 | #279141 | #009432 |
| 900 | #107d32 | #00ca50 |
| 1000 | #003a00 | #d8ffe4 |
语义颜色令牌
组件真正读取的那一层。这里的一切都通过上面的色阶解析,因此会自己跟着主题翻转。
| 令牌 | 解析到 | 职责 |
|---|---|---|
--ran-color-bg | --ran-background-100 | 页面背景 |
--ran-color-bg-subtle | --ran-background-200 | 轻微区分的区域 |
--ran-color-bg-elevated | --ran-background-100 · 暗色下为 gray-100 | 卡片、表面 |
--ran-color-bg-muted | --ran-gray-100 | 内凹 / 弱化填充 |
--ran-color-bg-hover | --ran-gray-200 | 悬停表面 |
--ran-color-bg-active | --ran-gray-300 | 激活(按下)表面 |
--ran-color-text | --ran-gray-1000 | 主要文字 |
--ran-color-text-secondary | --ran-gray-900 | 次要文字 |
--ran-color-text-disabled | --ran-gray-700 | 禁用文字 |
--ran-color-border | --ran-gray-400 | 默认边框 |
--ran-color-border-secondary | --ran-gray-300 | 更弱的边框 |
--ran-color-border-hover | --ran-gray-500 | 悬停边框 |
--ran-color-border-active | --ran-gray-600 | 激活边框 |
--ran-color-primary | --ran-gray-1000 | 主操作(无彩色) |
--ran-color-primary-hover | #383838 · 暗色 #cccccc | 主操作悬停 |
--ran-color-primary-active | #4d4d4d · 暗色 #b3b3b3 | 主操作按下 |
--ran-color-primary-text | --ran-background-100 | 主操作表面之上的文字 |
--ran-color-success | --ran-green-700 | 成功 |
--ran-color-warning | --ran-amber-700 | 警告 |
--ran-color-danger | --ran-red-700 | 危险 / 错误 |
--ran-color-link | --ran-blue-700 | 链接 |
--ran-color-primary-hover / -active 是语义层里仅有的两个字面量:它们是朝页面背景方向走的,而不 是沿着某条色阶走,所以暗色模式直接重定义了它们。
每个色彩语义只有一个含义
- 主操作是无彩色的——浅色下黑底白字,暗色下白底黑字(Geist 的品牌调性,
<r-button type="primary">)。其上的文字与图标用--ran-color-primary-text,会一起翻转。 这里没有单独的「contrast」令牌:主操作本身就是对比度最高的那一个。 - 蓝色是保留色,只用于链接(
--ran-color-link)与聚焦环,不是备选的主色。 - 绿色=成功 · 琥珀=警告 · 红色=危险,一色一义。
不存在 --ran-color-error,危险色叫 --ran-color-danger。var() 引用一个从未声明过的属性会解析 为「空」,整条声明被丢弃——而且是静默的,所以名字宁可对着表查,也别猜。
间距
元素之间的距离:padding、margin、gap。以 4px 为基数,只有九档:
| 令牌 | 值 | 令牌 | 值 |
|---|---|---|---|
--ran-space-1 | 4px | --ran-space-8 | 32px |
--ran-space-2 | 8px | --ran-space-10 | 40px |
--ran-space-3 | 12px | --ran-space-16 | 64px |
--ran-space-4 | 16px | --ran-space-24 | 96px |
--ran-space-6 | 24px |
数字是 4px 的倍数,所以档位是跳着的——没有 --ran-space-5。这正是重点:档位有限,页面才有节奏。
尺寸
元素自身的尺寸:图标大小、控件高度、小的方形/矩形控件。
| 令牌 | 值 | 典型用途 |
|---|---|---|
--ran-size-1 | 16px | 多选框方块、小号内联图标 |
--ran-size-2 | 18px | — |
--ran-size-3 | 20px | 控件内部的图标 |
--ran-size-4 | 24px | 工具栏图标按钮 |
--ran-size-5 | 28px | 紧凑控件高度 |
--ran-size-6 | 30px | — |
--ran-size-7 | 32px | 默认控件高度 |
这是刻意与间距分开的另一条尺度,混用会被机器校验拦下(sizing-scale 规则)。两者的取值范围和 递进方式不同——4px 翻倍式的间距尺度用在图标和控件尺寸上会得出别扭的数值;而且消费者必须能在不动另 一个的前提下单独调整其中一个:图标变大,不应该顺带把每一个恰好同值的间隙也撑开。某一档在数值上与 间距档位重合(--ran-size-4 和 --ran-space-6 都是 24px)只是巧合,不是别名。
真正一次性、没有别的组件共享的尺寸(比如某个菜单的 min-width),就保持为带自己字面量兜底的组件 令牌,不要硬塞进某一档。
排版
| 令牌 | 值 |
|---|---|
--ran-font-family | Geist / Geist Sans,其后是系统 UI 字体栈 |
--ran-font-mono | Geist Mono,其后是 ui-monospace、SF Mono、Menlo、Consolas… |
--ran-font-size | 14px——基准字号 |
--ran-line-height | 1.5715 |
排版按角色组织,角色一旦确定,字体、字号、字重、行高就一起定了:
| 角色 | 用于 | 字重令牌 | 字号令牌 |
|---|---|---|---|
| heading | 标题 | --ran-text-heading-weight(600) | --ran-text-heading-1..4(32/24/20/16px) |
| label | 单行、可扫读 | --ran-text-label-weight(500) | --ran-text-label-1..3(14/13/12px) |
| copy | 多行正文 | --ran-text-copy-weight(400) | --ran-text-copy-1..2(16/14px) |
| button | 按钮文字 | --ran-text-button-weight(500) | --ran-text-button-size(14px) |
| mono | 代码、数据 | --ran-text-mono-weight-regular(400)/ --ran-text-mono-weight-medium(500) | 复用 label / copy 档位 |
另有两个令牌只是为了让角色落地正确:
| 令牌 | 值 | 原因 |
|---|---|---|
--ran-text-heading-tracking | -0.03em | 大字号标题需要更紧的字距。 |
--ran-text-button-line-height | 1 | 定高控件内的文字才能锐利地居中。 |
Geist 的字重封顶在 600(semibold)——强调靠字号和留白,而不是更粗的字面。没有 --ran-text-copy-3:12px 那一档叫 --ran-text-label-3。
字体
ranui 自托管这两套字体(可变字重 100–900,SIL OFL 1.1 许可),一次引入即可,不依赖 CDN:
import 'ranui/fonts'; // 打包器<link rel="stylesheet" href="…/ranui/dist/fonts/fonts.css" />不引入也能正常工作,只是回退到系统字体栈。
圆角
| 令牌 | 值 | 用于 |
|---|---|---|
--ran-radius-sm | 6px | 控件——按钮、输入框、选择器 |
--ran-radius-md | 12px | 卡片、对话框 |
--ran-radius-lg | 16px | 大面积表面 |
--ran-radius-full | 9999px | 胶囊、头像 |
投影
投影是角色,不是装饰——按元素「是什么」来选层级。暗色模式会把三档全部替换,因为为白色页面调过 的投影放到黑色页面上就看不见了。
| 令牌 | 用于 | 浅色 | 暗色 |
|---|---|---|---|
--ran-shadow-elevated | 文档流内、同时带边框的表面——r-card、r-section | 0 1px 2px rgba(0,0,0,.04), 0 2px 4px -2px rgba(0,0,0,.05) | 0 1px 2px rgba(0,0,0,.16) |
--ran-shadow-menu | 浮在内容之上的临时层——下拉、选择面板、气泡卡片、toast | 0 2px 4px rgba(0,0,0,.05), 0 8px 24px -6px rgba(0,0,0,.14) | 0 1px 1px rgba(0,0,0,.2), 0 4px 8px -4px rgba(0,0,0,.4), 0 16px 24px -8px rgba(0,0,0,.5) |
--ran-shadow-modal | 阻塞式对话框——r-modal | 0 4px 12px rgba(0,0,0,.08), 0 20px 48px -12px rgba(0,0,0,.22) | 0 1px 1px rgba(0,0,0,.2), 0 8px 16px -4px rgba(0,0,0,.4), 0 24px 32px -8px rgba(0,0,0,.5) |
无边框的浮层只靠投影与页面拉开距离,所以浮层层级的投影必须有真实重量;浮层若回退到「抬起」层级, 看起来就像贴在页面上。
层级
浮层会 portal 到 <body>,因此需要明确的层级:
| 令牌 | 默认值 | 用于 |
|---|---|---|
--ran-z-modal | 1000 | 阻塞式对话框及其遮罩 |
--ran-z-dropdown | 1100 | 下拉 / 选择面板 / 气泡卡片——高于 modal,弹窗内的选择面板才不会被盖住 |
--ran-z-message | 1200 | toast 与通知——永远最上层 |
阶梯从 1000 起,是为了越过常规页面骨架(导航栏、遮罩通常在几十的量级)。可以在 :root 上整体覆盖, 也可以按组件覆盖(--ran-dropdown-host-z-index、--ran-modal-root-z-index、 --ran-message-z-index)——但不要用 !important。
动效
| 令牌 | 值 | 用于 |
|---|---|---|
--ran-motion-duration-fast | 0.15s | 悬停 / 激活状态过渡 |
--ran-motion-duration-base | 0.2s | 气泡、菜单 |
--ran-motion-duration-slow | 0.35s | 较大的展开 |
| 缓动令牌 | 曲线 | 性格 |
|---|---|---|
--ran-motion-ease-standard | cubic-bezier(0.645,0.045,0.355,1) | in-out,通用 |
--ran-motion-ease-snappy | cubic-bezier(0.33,0,0.15,1) | 干脆、无回弹——开关等小状态 |
--ran-motion-ease-spring | cubic-bezier(0.34,1.26,0.5,1) | 轻微回弹——按钮、卡片 |
--ran-motion-ease-bouncy | cubic-bezier(0.34,1.56,0.64,1) | 明显回弹——点赞、加入购物车 |
--ran-motion-ease-smooth | cubic-bezier(0.4,0,0.2,1) | 平缓、无回弹——展开与布局 |
spring 一族是从调好的 SwiftUI 弹簧参数(response/damping)蒸馏成的单次回弹贝塞尔曲线。
只把它们用在运动属性上——transform、opacity、盒模型几何属性。调色属性 (background-color、color、border-color、box-shadow、fill、stroke)刻意不带默认过渡: CSS 分不清「交互」和「主题翻转」,你给颜色加的淡入淡出,在明暗切换时同样会触发。每个组件仍然保留 --ran-*-transition 钩子,需要时可以自行开启。
聚焦
| 令牌 | 值 | 用于 |
|---|---|---|
--ran-focus-ring | 0 0 0 2px var(--ran-background-100), 0 0 0 4px var(--ran-blue-700) | 标准聚焦环,形式是 box-shadow |
--ran-focus-ring-inverse-color | #fff | 两套主题下都是深色的表面上的环颜色 |
聚焦环是双层的——内层用背景色,外层用蓝色——因此在任何表面上都清晰可见;而且它保持蓝色,不跟随已经 变成无彩色的主操作色。
--ran-focus-ring-inverse-color 刻意没有在暗色模式里重定义:它是给那种「无论页面主题如何、自身 表面始终是深色」的组件用的(r-player 覆盖在任意视频之上的控制条),而那种表面并不随页面主题变化。
皮肤基元
组件共享的、既不属于颜色也不属于尺寸和排版的少数结构性取值。刻意保持精简——这一层以前大得多,随 主题包一起被移除了大部分。
| 令牌 | 值 | 用于 |
|---|---|---|
--ran-skin-border-width | 1px | 组件绘制的边框宽度 |
--ran-skin-border-style | solid | 组件绘制的边框样式 |
--ran-skin-border-image-width | 4px | border-image-slice 的内缩,button/checkbox/input/modal/message 共享 |
--ran-skin-raised-shadow | var(--ran-shadow-elevated) | 抬起表面的投影,做一层间接以便皮肤替换 |
--ran-skin-font-family | var(--ran-font-family) | 组件使用的字体族,同样做了一层间接 |
暗色模式重定义了哪些
<html> 上的 data-ran-theme="dark"(也可以只作用于某棵子树,见主题系统) 只重定义基础色板,外加三处无法通过色阶解析的例外:
- 第一层的全部——gray、gray-alpha、blue、red、amber、green 的每一档,以及两个背景;
--ran-color-bg-elevated,暗色下指向--ran-gray-100,这样卡片才能从黑色页面上浮起来而不是融进去;--ran-color-primary-hover/-active,它们是字面量而不是色阶引用;- 三档投影,为深色底重新调过。
其余一切——所有其他语义令牌、所有尺寸、所有时长——都只定义一次。
组件令牌
语义层之下,每个组件还暴露自己的钩子,命名为:
--ran-{component}-{element}[-{state}]-{property}例如 --ran-btn-hover-background、--ran-select-search-active-border-width。它们默认指向语义令牌 ——var(--ran-btn-background, var(--ran-color-primary, #171717))——所以覆盖语义令牌能一次影响全部, 覆盖组件令牌则只改一个元素。
完整清单见仓库中的 style-tokens-public.md, 逐元素接口见元素 API。怎么覆盖见主题系统。
在自己的 CSS 里使用令牌
.panel {
background: var(--ran-color-bg-elevated);
color: var(--ran-color-text);
border: var(--ran-skin-border-width) var(--ran-skin-border-style) var(--ran-color-border);
border-radius: var(--ran-radius-md);
padding: var(--ran-space-4);
box-shadow: var(--ran-shadow-elevated);
}三条规则保证它暗色安全:
- 该跟随主题的值,不要写死 hex。
- 兜底值必须是会翻转的令牌——
var(--ran-color-text, var(--ran-gray-1000)),不要写var(--ran-color-text, #171717)。 - 兜底值引用的令牌必须存在,否则整条声明被丢弃,元素静默沿用继承来的样式。
组件库声明的每一个全局令牌都在本页列出;新增令牌若没有在这里记录,单元测试会失败。组件级令牌另行 生成,见 style-tokens-public.md。