Skip to content

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-dangervar() 引用一个从未声明过的属性会解析 为「空」,整条声明被丢弃——而且是静默的,所以名字宁可对着表查,也别猜。

间距

元素之间的距离:paddingmargingap。以 4px 为基数,只有九档

令牌令牌
--ran-space-14px--ran-space-832px
--ran-space-28px--ran-space-1040px
--ran-space-312px--ran-space-1664px
--ran-space-416px--ran-space-2496px
--ran-space-624px

数字是 4px 的倍数,所以档位是跳着的——没有 --ran-space-5。这正是重点:档位有限,页面才有节奏。

尺寸

元素自身的尺寸:图标大小、控件高度、小的方形/矩形控件。

令牌典型用途
--ran-size-116px多选框方块、小号内联图标
--ran-size-218px
--ran-size-320px控件内部的图标
--ran-size-424px工具栏图标按钮
--ran-size-528px紧凑控件高度
--ran-size-630px
--ran-size-732px默认控件高度

这是刻意与间距分开的另一条尺度,混用会被机器校验拦下(sizing-scale 规则)。两者的取值范围和 递进方式不同——4px 翻倍式的间距尺度用在图标和控件尺寸上会得出别扭的数值;而且消费者必须能在不动另 一个的前提下单独调整其中一个:图标变大,不应该顺带把每一个恰好同值的间隙也撑开。某一档在数值上与 间距档位重合(--ran-size-4--ran-space-6 都是 24px)只是巧合,不是别名。

真正一次性、没有别的组件共享的尺寸(比如某个菜单的 min-width),就保持为带自己字面量兜底的组件 令牌,不要硬塞进某一档。

排版

令牌
--ran-font-familyGeist / Geist Sans,其后是系统 UI 字体栈
--ran-font-monoGeist Mono,其后是 ui-monospace、SF Mono、Menlo、Consolas…
--ran-font-size14px——基准字号
--ran-line-height1.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-height1定高控件内的文字才能锐利地居中。

Geist 的字重封顶在 600(semibold)——强调靠字号和留白,而不是更粗的字面。没有 --ran-text-copy-3:12px 那一档叫 --ran-text-label-3

字体

ranui 自托管这两套字体(可变字重 100–900,SIL OFL 1.1 许可),一次引入即可,不依赖 CDN:

js
import 'ranui/fonts'; // 打包器
html
<link rel="stylesheet" href="…/ranui/dist/fonts/fonts.css" />

不引入也能正常工作,只是回退到系统字体栈。

圆角

令牌用于
--ran-radius-sm6px控件——按钮、输入框、选择器
--ran-radius-md12px卡片、对话框
--ran-radius-lg16px大面积表面
--ran-radius-full9999px胶囊、头像

投影

投影是角色,不是装饰——按元素「是什么」来选层级。暗色模式会把三档全部替换,因为为白色页面调过 的投影放到黑色页面上就看不见了。

令牌用于浅色暗色
--ran-shadow-elevated文档流内、同时带边框的表面——r-cardr-section0 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浮在内容之上的临时层——下拉、选择面板、气泡卡片、toast0 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-modal0 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-modal1000阻塞式对话框及其遮罩
--ran-z-dropdown1100下拉 / 选择面板 / 气泡卡片——高于 modal,弹窗内的选择面板才不会被盖住
--ran-z-message1200toast 与通知——永远最上层

阶梯从 1000 起,是为了越过常规页面骨架(导航栏、遮罩通常在几十的量级)。可以在 :root 上整体覆盖, 也可以按组件覆盖(--ran-dropdown-host-z-index--ran-modal-root-z-index--ran-message-z-index)——但不要用 !important

动效

令牌用于
--ran-motion-duration-fast0.15s悬停 / 激活状态过渡
--ran-motion-duration-base0.2s气泡、菜单
--ran-motion-duration-slow0.35s较大的展开
缓动令牌曲线性格
--ran-motion-ease-standardcubic-bezier(0.645,0.045,0.355,1)in-out,通用
--ran-motion-ease-snappycubic-bezier(0.33,0,0.15,1)干脆、无回弹——开关等小状态
--ran-motion-ease-springcubic-bezier(0.34,1.26,0.5,1)轻微回弹——按钮、卡片
--ran-motion-ease-bouncycubic-bezier(0.34,1.56,0.64,1)明显回弹——点赞、加入购物车
--ran-motion-ease-smoothcubic-bezier(0.4,0,0.2,1)平缓、无回弹——展开与布局

spring 一族是从调好的 SwiftUI 弹簧参数(response/damping)蒸馏成的单次回弹贝塞尔曲线。

只把它们用在运动属性上——transformopacity、盒模型几何属性。调色属性 (background-colorcolorborder-colorbox-shadowfillstroke)刻意不带默认过渡: CSS 分不清「交互」和「主题翻转」,你给颜色加的淡入淡出,在明暗切换时同样会触发。每个组件仍然保留 --ran-*-transition 钩子,需要时可以自行开启。

聚焦

令牌用于
--ran-focus-ring0 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-width1px组件绘制的边框宽度
--ran-skin-border-stylesolid组件绘制的边框样式
--ran-skin-border-image-width4pxborder-image-slice 的内缩,button/checkbox/input/modal/message 共享
--ran-skin-raised-shadowvar(--ran-shadow-elevated)抬起表面的投影,做一层间接以便皮肤替换
--ran-skin-font-familyvar(--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 里使用令牌

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);
}

三条规则保证它暗色安全:

  1. 该跟随主题的值,不要写死 hex
  2. 兜底值必须是会翻转的令牌——var(--ran-color-text, var(--ran-gray-1000)),不要写 var(--ran-color-text, #171717)
  3. 兜底值引用的令牌必须存在,否则整条声明被丢弃,元素静默沿用继承来的样式。

组件库声明的每一个全局令牌都在本页列出;新增令牌若没有在这里记录,单元测试会失败。组件级令牌另行 生成,见 style-tokens-public.md

Released under the MIT License.