生态产品多个设计器现已发布正式版

查看生态总览
跳转到内容

设计令牌与主题 ​

@xihan-ui/tokens 提供两部分能力:设计令牌(一份 CSS 自定义属性产物)与主题运行时(将用户偏好解析为根元素上的属性)。它位于依赖图的第一层,不依赖其他包。

令牌的两层 ​

primitive  ──►  semantic  ──►  组件私有槽
调色板本身      有语义的角色      单个组件的覆盖点

primitive:与场景无关的原始值,在任何作用域都不变,只声明一次。

css
--xh-color-brand-500: oklch(0.632 0.209 277);
--xh-color-neutral-950: oklch(0.145 0.005 258);
--xh-space-4: 16px;
--xh-radius-seed: 8px;

颜色使用 oklch 而不是 hex / hsl:同一明度的不同色相在感知上亮度一致,深色反转与对比度调整不需要逐色手工修正。

semantic:带语义的角色,指向 primitive。皮肤只能消费这一层。

css
--xh-bg-canvas: var(--xh-color-neutral-0);
--xh-bg-surface: var(--xh-color-neutral-0);
--xh-bg-brand: var(--xh-color-brand-600);
--xh-fg-default: var(--xh-color-neutral-900);
--xh-fg-muted: var(--xh-color-neutral-600);
--xh-border-subtle: color-mix(in oklab, var(--xh-ink) 4.7%, transparent);
--xh-control-h-md: 32px;
--xh-shape-control: var(--xh-radius-md);
--xh-elevation-floating: var(--xh-shadow-md);
--xh-motion-duration-enter: var(--xh-duration-normal);
--xh-layer-modal: var(--xh-z-modal);
--xh-overlay-max-w: 20rem;

语义层按角色分组:bg-* 背景、fg-* 前景、border-* 描边、control-* 控件尺寸、shape-* 形状、ring-* 焦点环、elevation-* 海拔、motion-* 时长与缓动、layer-* 层级、overlay-* 浮层尺寸、text-* 排版。

令牌源是 DTCG 格式的 JSON(packages/design/tokens/tokens/),产物由构建脚本生成,tokens.css / tokens.json / src/generated/tokens.ts 三份都入库。CI 会重新生成并比对产物,源与产物不同步时构建失败。

形状阶梯 ​

圆角只有一套语义阶梯,组件不写独立圆角值。

令牌值用途
--xh-shape-inset2px嵌在控件里的内层:勾选方框、页内列表的候选行、字段内的清空钮、色块
--xh-shape-control2px控件本体:Button、Input、Select Trigger、Toggle、kbd、tooltip、单选组 segmented 形态的轨道;方签 Tag、状态方签与 Alert 提示条
--xh-shape-surface4px成面的静态容器:Card、Panel、列表容器、Tabs segment 的轨道、分页的描边方块
--xh-shape-overlay4px脱离文档流的浮层:Popover、Menu、Dialog、Notification 卡片预设、Drawer 朝向页面的两个角(贴边的两个角不取圆角,轻提示预设取 control);引用 surface,只改承载面圆角时浮层跟着走
--xh-shape-circle50%正圆:头像、圆形图标按钮、单选指示器
--xh-shape-pill9999px胶囊:Badge 等状态 chip(Tag 与结果标记是方签,取 control),以及轨道、指示条、手柄、滚动条滑块等一维对象

形状令牌引用原始阶梯 --xh-radius-sm / md / lg(2 / 4 / 8px):inset 与 control 取 sm,surface 取 md,overlay 引用 surface。普通按钮、字段、卡片与浮层不使用 pill,Tag 与结果标记是方签;锚定浮层里的列表行与 Command、Transfer 的列表行是通栏行,不取圆角;内层圆角不超过外层圆角减去内边距;相连控件消除相接侧圆角。亮色、暗色与紧凑密度不改变形状身份。正方盒取 circle,不用 pill 冒充圆。

点击触感 ​

离散操作控件(Button、Toggle、图标按钮、分页按钮、工具栏按钮)共用一套按压时间线:

阶段时长缓动结果
按下--xh-motion-duration-press(150ms)--xh-motion-ease-press背景进入 active;scale 1 → --xh-motion-scale-press(缺省 1,只换面)
释放--xh-motion-duration-release(200ms)--xh-motion-ease-releasescale 回到 1;背景、描边、字色按 --xh-motion-duration-micro 回到 hover / rest

缺省只换面不缩放;要按下回弹,在根上把 --xh-motion-scale-press 写回 0.97,时间线不用改。

Menu Item、Listbox Item、Tree Node、Table Row 等集合项与 Accordion / Collapsible 等 disclosure trigger 使用同一节奏(按下 150ms 换到 active 面,松开按 micro 回到 hover / rest),但只切换表面,不缩放整条,也不允许零反馈。减少动效时 --xh-motion-scale-press 归 1、两段时长归 1ms,颜色反馈保留。

组件内滚动 ​

滚动条形态只有两档,按滚动面的身份固定,不按组件各自决定:

档适用面令牌
自绘条(Scrollbar 组件接线)Overlay 家族 positioner 下的 content / list / column;定高小列表(Listbox content、Transfer list、时间列、Cascader column)type 默认 scroll-hover;厚度浮层 --xh-scrollbar-thickness-sm(4px)、页内 --xh-scrollbar-thickness-md(6px);壳上 --xh-scrollbar-track-bg: transparent
原生细条页内结构容器(Table、Tree、Transfer 面板、Virtualizer viewport、Dialog / Drawer / FloatingPanel body、Layout sider / content、SideNav popout、Log / MessageFeed 视口、日历年网格、Typography pre)与作者自建滚动容器reset 层 :where([data-scope][data-part], [data-xh-scroll]) 统一给 scrollbar-width: thin + scrollbar-color: var(--xh-fg-scrollbar-thumb) var(--xh-bg-scrollbar-track)

滑块色阶维持三级:--xh-fg-scrollbar-thumb / -hover / -active 分别是前景色 15% / 25% / 35%,两档共用。作者自己的滚动容器加 data-xh-scroll 即得同一套细条,写法与边界见皮肤与样式分层。

边界行为按身份给:overscroll-behavior: contain 只给浮层滚动面、模态 body 与粘底视口,页内结构容器保持 auto;scrollbar-gutter: stable 只给内容高度动态变化的容器(Log、MessageFeed、Dialog / Drawer body),并带 :not([data-xh-scrollbar]) 守卫;边缘渐隐只在 ScrollArea 的 fade 变体与 Marquee 提供。文档站页面滚动条与组件滚动条同一 type,不另写覆写。

排版角色 ​

text-* 排版令牌按角色取用,层级由字号、字重、行高和间距共同表达,不能只调颜色:

角色字号 / 字重 / 颜色与相邻元素的间距
字段标签(单字段与 Slider、Rating、Signature、Color* 等复合单字段)--xh-text-label-size 14 / --xh-text-label-weight 400 / --xh-fg-muted竖排与控件 --xh-space-2;Form 横排时标签列占一行的 5 / 24、与控件 --xh-space-4
集合标题(RadioGroup、CheckboxGroup、Listbox、Tree、TagGroup、Descriptions)14 / --xh-text-label-weight 400 / --xh-fg-muted与集合 --xh-space-2
字段说明 / helper(Field、Fieldset)--xh-text-caption-size 12 / --xh-fg-subtle / --xh-leading-normal;禁用不另变色紧贴控件,辅助行最小高 --xh-space-5
其余说明(Card、Alert、Dialog 等面内的 description)--xh-text-secondary-size 13 / --xh-fg-muted / --xh-leading-normal;Steps 的步骤说明取次级标注 12 / --xh-fg-subtle—
错误文案12 / --xh-fg-danger与说明共用控件下方那一行辅助行
浮层内与小面标题(Popover、HoverCard、Popconfirm、FloatingPanel、Approval、EmptyState、Citation)14 / --xh-font-weight-medium—
区块与面板标题(Card、Descriptions、Alert、Notification、Steps;Dialog、Drawer、Tour)heading-3(--xh-text-heading-3-*):16 / 500—
页面标题(PageHeader)heading-2(--xh-text-heading-2-*):20 / 500—
大号数值(Statistic、Timer、NumberAnimation)sm / md / lg 依次取 heading-2 / heading-1 / display(--xh-text-display-size):20 / 24 / 28—
次级标注(计数、快捷键、时间戳、序号)--xh-text-caption-size 12—

必填星号与错误文案是公共层规则:星号 --xh-glyph-mark-required 排在标签文字之前,取 --xh-text-caption-size + --xh-fg-danger、与文字隔 --xh-space-1,自带标签的字段不各画一套;字段标签与字段说明禁用不另变色,其余标签禁用取 --xh-fg-subtle,单行标签 --xh-leading-none。控件内图标随 size 档取 --xh-glyph-size-sm / md / lg(16 / 18 / 20);--xh-glyph-size-text(随文 1em)只给 Tag、Kbd、Breadcrumb、Typography、Highlight 这类纯行内文字组件。

组件不定字体,font-family 照常继承宿主。库的缺省无衬线栈收在 --xh-font-family-sans(系统界面字体打头,再给苹方、冬青黑、微软雅黑与思源黑体兜中文),只在引入可选入口时落到页面根上:

ts
import "@xihan-ui/styles/global.css";

这份入口不在 @xihan-ui/styles 主入口里;规则放在最低的 xihan.reset 层、选择器包在 :where() 里,宿主自己写的字体照样压得住。代码与等宽数字取 --xh-font-family-mono。

八轴视觉环境运行时 ​

VisualEnvironmentController 是 mode、brand、density、dir、contrast、motion、transparency、material 的唯一状态与 DOM 投影入口。一次 setPreference 同步提交同一 scope 的完整八轴:

ts
import { setMotionOverride } from "@xihan-ui/motion";
import {
  brandId,
  createMotionOverrideSink,
  createVisualEnvironmentController,
} from "@xihan-ui/tokens/runtime";

const visual = createVisualEnvironmentController({
  root: document.documentElement,
  storageKey: "app-visual-environment",
  onStorageError: detail => reportStorageFailure(detail),
  motionSink: createMotionOverrideSink(setMotionOverride),
  initial: {
    mode: "system",
    brand: brandId("xihan"),
    density: "comfortable",
    dir: "ltr",
    contrast: "system",
    motion: "system",
    transparency: "system",
    material: "standard",
  },
});

visual.setPreference({ mode: "dark", density: "compact" });
visual.getState();
visual.subscribe(state => console.log(state));
visual.dispose();

启用 storageKey 必须同时给 onStorageError。解析失败、隐私模式或配额不足会明确回调;控制器继续维护内存状态,但不会把持久化失败伪装成成功。

根控制器的 motionSink 是显式依赖注入:它把解析后的 motion 同步到 Presence、平滑滚动和 JS 动画。只有 documentElement 根作用域允许传;局部控制器只改当前 DOM scope,不能隐式污染全局 JS override。

八轴状态会投影成 Portal 可桥接的完整属性面:

html
<html data-theme="dark" data-brand="xihan" data-density="compact" data-contrast="more" data-motion="reduce" data-transparency="reduce" data-material="liquid" dir="rtl">

偏好、状态与系统轴 ​

VisualEnvironmentPreferenceVisualEnvironmentState
含义用户或服务端提交的意图已完全定型的事实
undefined清除本层覆盖,继承父控制器不存在,八轴全部非空
'system'仅 mode / contrast / motion / transparency 合法不存在,已解析成具体值

brand、density、dir、material 没有平台媒体查询,因此不接受伪造的 system。其余四轴分别读取 prefers-color-scheme、prefers-contrast、prefers-reduced-motion、prefers-reduced-transparency。没有 window / matchMedia 的 SSR 环境使用浅色、默认对比度、默认动效和默认透明度,不猜测客户端偏好。

基线八轴为 light、xihan、comfortable、ltr、default、default、default、standard。contrast='default' 是显式恢复常规对比度的正式值;旧的 base 不再接受。

DOM 轴说明
data-theme明暗两值各有整套语义取值;system 是不跑 JS 的跟随系统档,见下文
data-brand切换品牌色:预设品牌直接可用,自定义品牌先 registerBrand
data-density='compact'收紧控件高度、内距与间隙,不缩字号和字形
dir由逻辑属性驱动 LTR / RTL 布局;只认物理方向的量(translate、渐变角度、clip-path 左右两侧)乘 --xh-direction-sign 换向——它按就近的 dir 继承(ltr 为 1、rtl 为 -1),局部写回 ltr 的子树跟着翻回;transform-origin 这类物理坐标取 --xh-direction-start / --xh-direction-end(行内起始缘 / 末尾缘的水平位置,ltr 为 0% / 100%,rtl 反过来)
data-contrastmore 加强边界;default 显式回到常规档
data-motionreduce 触发局部 CSS 动效降级
data-transparencyreduce 把主题开出的透景材质切回实体配方
data-materialliquid 让浮在内容之上的导航层部件(浮动钮、媒体控制、吸顶顶栏)换成液态面;standard 保持原材质,最近的祖先生效

不跑 JS 跟随系统明暗 ​

控制器的 mode: "system" 要等脚本跑起来才解出明暗。页面只想跟随系统、不想为此引脚本,或者要在脚本到达之前就对,直接在 HTML 里写 data-theme="system":

html
<html data-theme="system">

浅色系统下它与 light 同一套取值;系统切到深色,令牌的深色取值、color-scheme 与皮肤里只在深色下成立的几条规则(语气、按钮实底等)由 prefers-color-scheme 一并切过去,不经任何脚本,系统随时切换随时跟上。system 只是写在 DOM 上的一档,控制器照旧只投影 light / dark:之后若在同一个根上建控制器,它会把属性改成解出的具体值。

预设品牌 ​

@xihan-ui/tokens 自带几档有名字的品牌,清单是 BRAND_PRESETS(如 blue、green、orange、pink),类型是 BrandPreset;BRAND_PRESET_COLORS 给每档一个代表色(那一档的品牌 600),画色板格子、预设清单时用。每档在 tokens.css 里是一个 [data-brand='<id>'] 取值块,只换品牌原语,不必 registerBrand:

ts
visual.setPreference({ brand: brandId("green") });

各档取基础色板里同名的色相,每一级按缺省品牌那一级的相对亮度派生;indigo 是原来的缺省品牌(色相 258),缺省换成色相 277 的靛蓝后留作预设。品牌参与的对比度(实心底上的字、品牌字色、品牌淡底与上面的字、聚焦环)因此与缺省品牌同样达标,浅色、深色与两档高对比都由门禁逐档核对。registerBrand 注入的同名取值块不在层里,压过预设。

data-brand 要与 data-theme 落在同一个元素上才换得动品牌:语义令牌在主题边界上声明,只在子元素上换品牌原语,语义令牌仍是外层解好的值。可视环境控制器的根总是同时写这两个属性;手写时在同一个元素上一并写 data-theme。

父作用域、Portal 与回收 ​

局部控制器通过 parent 明确继承。父状态变化时,子控制器只重算没有本地偏好的轴;setPreference({ motion: undefined }) 会删除本层覆盖,立即恢复继承。dispose 会退订父作用域和系统媒体,并把根元素八个属性精确还原到接管前。

ts
const page = createVisualEnvironmentController({
  root: document.documentElement,
  initial: { mode: "dark", motion: "system" },
});
const editor = createVisualEnvironmentController({
  root: document.querySelector("#editor")!,
  parent: page,
  initial: { density: "compact", transparency: "reduce" },
});

React / Vue 的 XhConfigProvider / provideXhConfig 通过 config.visualEnvironment 接受带显式 root 的控制器选项;嵌套 Provider 自动接父控制器。Web Components 的 <xh-config> 自身就是局部 scope,可直接设置 mode、brand、density、direction、contrast、motion、transparency、material。物理 Portal 仍由 Core 的 VisualBridge 桥接已解析八轴,适配器不另存一份视觉状态。

五轴旧视图 ​

createThemeController、resolveTheme 与 applyThemeAttrs 继续存在,但只是新控制器的明确五轴视图:没有独立 resolver、媒体查询或持久化实现。新代码应使用八轴控制器;独立的 applyThemeAttrs 只写五轴,控制器则始终维护完整八轴 DOM 合同。

color-scheme 声明写在深色取值块里,所以嵌套深色区域的原生控件也会跟随。SSR 首屏应直接输出八个已解析属性,避免客户端接管前闪烁。

材质配方 ​

令牌配方五档,每档都提供同名九项令牌:bg、backdrop、border、highlight、shadow、separator、fg、fg-muted、focus-surface;M5 liquid 另有五项专用令牌。缺省各档都是实体面;透景(半透明底 + 背景滤镜)只允许主题给瞬态浮层开;不提供玻璃材质,也不提供任何兼容别名。另有 raised / floating 两个由海拔令牌组成的叠加档:它们没有 --xh-material-* 令牌,只能写成 solid 描边 + solid 底 + 对应海拔的组合。

编号令牌用途光学
M0 solid--xh-material-solid-*静态内容面缺省:border-default 描边 + surface 底 + 无影;字段静息同为描边式,铺 --xh-bg-field(缺省白底)实体底色,无高光、无投影
M1 soft--xh-material-soft-*柔和淡底的次级面;不用于 Card 与字段。Button 与 Tag 是平面面,不取它的顶光与投影实体底色,细微顶光与两段接触投影,无背景模糊
M2 frosted--xh-material-frosted-*短列表、菜单、tooltip、气泡等锚定瞬态浮层;含网格或多列的锚定面板改用 solid + border-default + --xh-elevation-floating实体 surface 底,无背景模糊,1px border-default 描边,md 档投影,无顶光
M4 elevated--xh-material-elevated-*Dialog、Drawer、Command、Tour、Notification 等模态与强反馈面(sheet),必有 1px 描边实体 surface 底,无背景模糊,lg 档投影
M5 liquid--xh-material-liquid-*只在 data-material="liquid" 下出现:浮在内容之上的导航层——浮动钮(FloatButton、BackTop、MessageFeed / Log 的回到底部)、媒体控制(Carousel 控制钮与分页条、ImageViewer 控制层)、吸顶的 Layout 顶栏可读下限浅 0.48 / 深 0.61 不透明度,8px 模糊,140% 饱和度,墨色细线 + 1px 边缘光,Chromium 下边缘折射
raised(叠加档)solid 描边 + solid 底 + --xh-elevation-raisedCard 与可抬起 / 可拖起部件(Tabs segment 的滑块、开关拇指等),逐部件登记;描边必须在,影只是加成,只有可交互时允许 hover 抬升实体底色;--xh-elevation-raised 取 sm 档
floating(叠加档)solid 底 + --xh-border-default + --xh-elevation-floating含网格或多列的锚定面板:NavigationMenu content、Date / Time / DateRange / TimeRange picker content实体底色,不透景,md 档投影
后缀用途
bg / backdrop底色 / 背景滤镜
border / highlight / shadow外边界、顶部高光与投影
separator内部分段线
fg / fg-muted正文与次要文字,始终不透明
focus-surface键盘聚焦时铺在焦点环内侧的实体隔离底

M2 frosted 在库里只有一份实现:材质家族配方 @xihan-ui/styles/material.css。组件在部件上投影 data-xh-material="frosted",锚定浮层的内容面由配方画描边、底、前景与投影,背景滤镜与 1px 顶光两条通道缺省为空、留给要透景的主题;浮动钮的面归 Action Control,由配方给交互阶梯(悬停 / 按下换不透明淡底一档、二档,键盘聚焦铺 focus-surface)。自定义的浮层面写同一个属性即可接入,用 --xh-frosted-bg、--xh-frosted-border、--xh-frosted-fg、--xh-frosted-shadow、--xh-frosted-backdrop、--xh-frosted-highlight 覆盖。

Card 只有三档形态:outline(缺省,solid 描边 + surface 底 + --xh-elevation-raised,sm 档)、subtle(--xh-bg-subtle + 透明占位边 + 无影)、ghost(不写边、底与影,只允许分隔线)。边界只由描边承担,阴影与淡底都不作为边界。

环境轴在同一令牌名上原位降级:高对比档加强边界;减少透明度与打印强制实体底、关闭背景滤镜(主题开了透景也一样);强制色改由 Canvas / CanvasText 表达。四档在浅色、深色两套主题下都按黑、白、中灰、页面与品牌背景验证正文至少 4.5:1、焦点环对隔离底至少 3:1。

主题与对比度可以分别声明在不同层级:每一层采用最近的 data-theme 和 data-contrast。子主题不撤销祖先的高对比度,data-contrast="default" 可以局部恢复常规档;兄弟区域互不影响。

frosted 浮层面的使用范围 ​

  • 名字沿用 frosted,缺省取值是实体弹出层:不采样背后内容,没有顶部边界光,1px 描边承担可见边界,不能只靠投影分层。
  • 只用于锚定瞬态浮层,正文、表单主体、Card、Table、Notification 与 Dialog 主阅读面不使用。
  • 主题要磨砂透景时,给 --xh-material-frosted-bg 半透明底、给 backdrop 一档 blur(不高于 16px,saturate 不高于 1.08)、给 highlight 不超过 1px 的内侧顶光;开了透景就不把 backdrop-filter 放在页面根、大滚动区或重叠的多层表面,不对模糊半径做动画,焦点环搭配 focus-surface 对应的实体隔离面。

紧凑配方(Tooltip) ​

Tooltip 不反白,与 Popover 同一副浮层面:底、字、边取 frosted 材质(M2),再用三支 compact 配方组合;写了 tone 才换语气实底,那时边改取 on 色 20% 拼色承担 1px 可见边界(中性描边压在彩色底上看不见):

配方常规值
--xh-material-frosted-compact-alpha不透明(--xh-alpha-opaque)
--xh-material-frosted-compact-backdropnone,无模糊
--xh-material-frosted-compact-shadowmd 档投影(浅色引 --xh-shadow-md,深色是同样三层的深色投影)

高对比、强制颜色和打印去掉投影;减少透明时保留小投影表达浮层位置。组件只经这三支配方消费光学参数,不直接选择 alpha 或 blur 原语。

液态材质 ​

data-material 是应用级材质轴,缺省 standard,取 liquid 时浮在内容之上的导航层部件换成液态面。它写在任意祖先上,最近的一层生效,Portal 视觉桥会把它带到实例壳;standard 档下同一部件保持原材质。用了视觉环境控制器时,经它的 material 偏好设置(控制器始终维护根上的这个属性)。

现有的液态部件:FloatButton 触发器与展开的动作、BackTop、MessageFeed / Log 的回到底部、Carousel 的翻页与播放钮和分页条(托在一条液态胶囊上)、ImageViewer 的工具条、计数、翻页与关闭钮,以及 Layout 固定时的顶栏。瞬态浮层、模态、Notification、Card、Table、表单与正文容器不用液态,由门禁 check-material-scope 守住。

html
<html data-material="liquid">
  <!-- 图片、视频、画布这类读不到颜色的区域,由作者声明下层的明暗 -->
  <section data-xh-backdrop="dark" data-xh-backdrop-busy>…</section>
</html>

作者只写这一个属性。组件挂载后由 @xihan-ui/core/visual-environment 的液态面接管,同一文档的部件共用一套监听,不需要额外安装或调用:

  • 按部件下层的计算底色与作者声明选色调:下层亮取浅色调、下层暗取深色调,部件随之成为黑墨或白墨域(相对亮度 0.179 ± 0.04 滞回,内容滚过分界附近不来回闪)。
  • 下层均匀且色调已知时换通透档(浅 0.24 / 深 0.34);下层有文字、渐变、图片、视频、画布或读不到时留在可读下限,标签在任何下层上至少 4.5:1。页面里有被设为 inert 的内容(命中栈里没有它们)时同样按读不到处理。
  • 滚动、窗口缩放、图片加载完、过渡或动画播完、状态机驱动的平移落定(轮播翻页、看图惯性)时重读下层。
  • 细指针移动时 1px 边缘光转向指针;粗指针、无指针与减弱动效下固定左上(RTL 右上)。
  • Chromium 内核下距边缘 18px 以内折射下层;其余引擎、尺寸超过 640 × 120 或同一视口超过 3 个时只模糊不折射。
专用令牌用途
--xh-material-liquid-tint通透档的着色
--xh-material-liquid-alpha-clear / -alpha-floor通透档与可读下限的不透明度
--xh-material-liquid-rim-far背光一侧的弱亮边
--xh-material-liquid-bezel折射带宽度

服务端与挂载前输出静态形态:色调随主题,不透明度取可读下限。减少透明、打印时不透明度 1 且无背景滤镜与折射,高对比档不透明度 1,强制色改由系统色表达。

直接取用令牌 ​

ts
import type { TokenName } from "@xihan-ui/tokens";
// TypeScript 侧带类型的令牌名
import { tokens } from "@xihan-ui/tokens";

// 机读产物:生成 Figma 变量、Tailwind 主题、设计稿标注都可以用
import tokens from "@xihan-ui/tokens/tokens.json" with { type: "json" };

相关 ​

Released under The MIT License