网站导航

编写样式

主题令牌

定义共享设计值,观察令牌变更如何影响使用方,并选择局部覆盖、静态交付和 inline 替换。

本页内容

#概览

当多个元素应一起变化时,就定义令牌,例如卡片间距、操作颜色或阅读列宽。将其放入应用样式表中现有 @import '@master/css' 之后的 @theme,再在标记中使用匹配类名。

三个共享值,一张卡片

@theme {  --color-brand: #4f46e5;  --spacing-card: 1.5rem;  --radius-card: 0.5rem;}
生成的 CSS
@layer theme {  :root,  :host {    --color-zinc-30: oklch(78% .0025 265);    --color-zinc-40: oklch(68.6% .0028 265);    --color-zinc-60: oklch(48.6% .003 265);    --color-zinc-70: oklch(39.8% .0028 265);    --color-zinc-90: oklch(24.6% .0022 265)  }  :root,  :host {    --color-surface-raised: light-dark(var(--color-white), var(--color-zinc-90));    --color-text-body: light-dark(var(--color-zinc-70), var(--color-zinc-30));    --color-text-muted: light-dark(var(--color-zinc-60), var(--color-zinc-40))  }  :root,  :host {    --color-white: oklch(100% 0 none)  }  :root,  :host {    --font-weight-regular: 400;    --font-size-xs: .75rem;    --font-size-sm: .875rem;    --font-size-lg: 1.125rem;    --text-xs: var(--font-size-xs);    --text-xs--line-height: 1.5;    --text-xs--letter-spacing: 0;    --text-sm: var(--font-size-sm);    --text-sm--line-height: 1.5;    --text-sm--letter-spacing: 0;    --text-lg: var(--font-size-lg);    --text-lg--line-height: 1.5;    --text-lg--letter-spacing: 0;    --spacing-xs: .5rem;    --spacing-sm: .75rem;    --container-sm: 24rem  }  :root,  :host {    --shadow-sm: 0 0 0 1px light-dark(oklch(0% 0 none / .04), oklch(100% 0 none / .05)), 0 1px 1px -.5px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .3)), 0 2px 2px -1px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .26)), 0 4px 4px -2px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .24))  }  :root,  :host {    --color-brand: #4f46e5;    --spacing-card: 1.5rem;    --radius-card: .5rem  }}@layer utilities {  .text-lg {    font-size: var(--text-lg);    line-height: var(--text-lg--line-height, normal);    letter-spacing: var(--text-lg--letter-spacing, normal)  }  .text-sm {    font-size: var(--text-sm);    line-height: var(--text-sm--line-height, normal);    letter-spacing: var(--text-sm--letter-spacing, normal)  }  .text-xs {    font-size: var(--text-xs);    line-height: var(--text-xs--line-height, normal);    letter-spacing: var(--text-xs--letter-spacing, normal)  }  .items-center {    align-items: center  }  .bg-brand {    background-color: var(--color-brand)  }  .bg-surface-raised {    background-color: var(--color-surface-raised)  }  .r-card {    border-radius: var(--radius-card)  }  .pill {    border-radius: calc(infinity * 1px)  }  .shadow-sm {    box-shadow: var(--shadow-sm)  }  .fg-text-body {    color: var(--color-text-body)  }  .fg-text-muted {    color: var(--color-text-muted)  }  .flex {    display: flex  }  .font-regular {    font-weight: var(--font-weight-regular)  }  .gap-xs {    gap: var(--spacing-xs)  }  .h\(8\) {    height: calc(8 * 1px)  }  .m\(0\) {    margin: calc(0 * 1px)  }  .mx-auto {    margin-inline: auto  }  .mb-xs {    margin-bottom: var(--spacing-xs)  }  .mt-sm {    margin-top: var(--spacing-sm)  }  .max-w-sm {    max-width: var(--container-sm)  }  .p-card {    padding: var(--spacing-card)  }  .w\(8\) {    width: calc(8 * 1px)  }}
p-card 和 r-card 设置共享内边距和圆角。小标记使用 bg-brand,让颜色令牌可见,同时保留卡片的中性色表面。

p-card 会生成 padding: var(--spacing-card);浏览器在卡片元素上解析自定义属性。所有使用方应一起变化时就修改令牌;变化只属于单个元素时则使用局部值。

#改变一个值,更新所有使用方

--spacing-card 可以同时提供卡片内边距和列表间隔。将值从 1.5rem 改为 2rem,两处用途都会改变,无需编辑标记:

<article class="p-card">Field notes</article><ul class="flex gap-card">  <li>Draft</li>  <li>Ready</li></ul>

一个共享值,同时改变内边距和间隔

两列的标记完全相同。测量假设根字体大小为 16px;高亮区域以实际相对尺寸显示内边距和间隔。

文字说明
  1. 变更前. 根字体为 16px 时,--spacing-card: 1.5rem 通过 p-card 提供 24px 内边距,并通过 gap-card 提供 24px 间隔。
  2. 变更后. 将令牌改为 2rem,两个测量值都增至 32px,无需编辑类名。
  3. 选择联动关系. 只有这些间距应一起变化时,才共享一个令牌。不同设计决策需要不同令牌,即使当前数值相同也一样。

这种联动是设计决策。如果卡片应更宽松而列表间隔要保持不变,请使用不同令牌。现在共享同一数值,并不代表界面的两个部分永远都应一起变化。


#按命名空间命名令牌

令牌前缀决定命名空间,各类名系列读取自己支持的命名空间。--spacing-card 提供给 p-card、m-card 和 gap-card;--radius-card 提供给 r-card;--container-panel 提供给 max-w-panel。相同键名在不同系列可代表不同值:p-lg 读取间距,r-lg 读取圆角。

index.css
@theme {  --color-text-action: var(--color-brand);  --font-family-sans: Geist, ui-sans-serif, system-ui, sans-serif;  --container-panel: 28rem;}

只有 @theme 声明会注册类名令牌;其他位置的原生自定义属性不会。按共享设计决策命名,例如 brand 或 card,不要按首次使用它的组件命名。重新定义 --spacing-lg 等预设键名即可改变整个项目的值。系列索引列出各类名读取的命名空间;解析方式请参阅变量与模式。


#令牌别名与覆盖

需要让一个令牌跟随另一个时,使用 var()。别名先解析再继承:如果 --color-text-action 声明在根元素,在后代覆盖 --color-brand 不会重新绑定继承的别名。请在该作用域重新声明别名。

局部品牌色与继承的别名

@theme {  --color-brand: light-dark(#1d4ed8, #93c5fd);  --color-text-action: var(--color-brand);}@layer components {  [data-brand="plum"] {    --color-brand: light-dark(#86198f, #f0abfc);  }  [data-rebind-action] {    --color-text-action: var(--color-brand);  }}
生成的 CSS
@layer components {  [data-brand="plum"] {    --color-brand: light-dark(#86198f, #f0abfc);  }  [data-rebind-action] {    --color-text-action: var(--color-brand);  }}@layer theme {  :root,  :host {    --color-zinc-90: oklch(24.6% .0022 265)  }  :root,  :host {    --color-surface-raised: light-dark(var(--color-white), var(--color-zinc-90))  }  :root,  :host {    --color-white: oklch(100% 0 none)  }  :root,  :host {    --font-weight-regular: 400;    --font-size-sm: .875rem;    --font-size-lg: 1.125rem;    --text-sm: var(--font-size-sm);    --text-sm--line-height: 1.5;    --text-sm--letter-spacing: 0;    --text-lg: var(--font-size-lg);    --text-lg--line-height: 1.5;    --text-lg--letter-spacing: 0;    --radius-lg: .5rem;    --spacing-sm: .75rem;    --spacing-md: 1rem;    --spacing-lg: 1.5rem  }  :root,  :host {    --color-brand: light-dark(#1d4ed8, #93c5fd);    --color-text-action: var(--color-brand)  }}@layer utilities {  .text-lg {    font-size: var(--text-lg);    line-height: var(--text-lg--line-height, normal);    letter-spacing: var(--text-lg--letter-spacing, normal)  }  .text-sm {    font-size: var(--text-sm);    line-height: var(--text-sm--line-height, normal);    letter-spacing: var(--text-sm--letter-spacing, normal)  }  .bg-surface-raised {    background-color: var(--color-surface-raised)  }  .r-lg {    border-radius: var(--radius-lg)  }  .fg-brand {    color: var(--color-brand)  }  .fg-text-action {    color: var(--color-text-action)  }  .font-regular {    font-weight: var(--font-weight-regular)  }  .m\(0\) {    margin: calc(0 * 1px)  }  .mt-md {    margin-top: var(--spacing-md)  }  .mt-sm {    margin-top: var(--spacing-sm)  }  .mb\(0\) {    margin-bottom: calc(0 * 1px)  }  .p-lg {    padding: var(--spacing-lg)  }}
两个操作标签都使用 fg-text-action。第一个继承根元素计算的别名;第二个在 --color-brand 为梅紫色的范围中重新声明别名。

继承的别名保留其计算值

蓝色和紫色标记表示两条值解析路径。这是作用域图,不是额外的主题切换。

文字说明

根元素将 action 别名解析为蓝色。子元素将 brand 改为梅紫色:继承的别名仍为蓝色,直到在该子元素重新声明。

需要跟随浅色和深色模式的颜色时,让令牌使用 light-dark() 值,并保持不指向特定模式的名称,例如 --color-surface-card。启用模式请参阅颜色。


#选择令牌交付和值替换方式

这两个修饰符回答的是不同问题:

需求选择结果
项目使用时才输出值普通 @theme按需输出声明;类名保留令牌引用。
即使未发现使用方也保留值@theme static令牌声明仍可供外部集成等用途使用。
在每个应用样式的元素上解析引用变量@theme inline在生成声明中替换一次令牌值。

如果集成读取的值无法被静态发现过程识别,请使用 static:

@theme static { --color-integration-brand: #4f46e5; }

需要在使用类名的元素上解析原生变量时,可使用 inline 建立连接:

@theme inline { --color-brand: var(--app-brand); }:root { --app-brand: #4f46e5; }[data-theme="ocean"] { --app-brand: #0369a1; }
<section data-theme="ocean">  <div class="bg-brand">Ocean background</div></section>

此处 bg-brand 会输出 background-color: var(--app-brand),因此内层元素读取从 section 继承的 ocean 值。替换移除了中间的 --color-brand 引用,并非将固定颜色复制到每个元素。

两种行为都需要时,使用 @theme static inline 组合修饰符。Inline 替换适用于类名和 @apply;手写原生 CSS 会保留原有引用。请参阅主题契约。


分别选择保留与替换

static 控制保留;inline 控制替换。使用其中一种并不代表同时启用另一种。

文字说明

两条互相垂直的轴区分没有使用方时保留令牌,以及将令牌值替换到生成声明中。

#定义动效令牌

--animate-* 令牌是完整的 animation 简写,因此可以组合关键帧名称、持续时间和缓动令牌。在旁边使用原生 CSS 编写关键帧:

动效由实际操作触发

对话框打开时只入场一次。按 Escape 或 Close details 会将焦点还给触发按钮。启用减少动态效果时会立即打开。模态边界限制在此预览内部。
index.css
@theme {  --duration-enter: 180ms;  --easing-emphasized: cubic-bezier(.16, 1, .3, 1);  --animate-dialog-in: dialog-in var(--duration-enter) var(--easing-emphasized) both;}@keyframes dialog-in {  from { translate: 0 0.5rem; opacity: 0; }}
<dialog class="animate-dialog-in@motion-safe">…</dialog>

此令牌没有指定重复次数,因此入场只执行一次;预设的 animate-* 令牌则会重复。用户请求减少动态效果时,@motion-safe 会让对话框保持静止。关键帧默认保留在样式表中;使用 @prune native 时,被引用的动画令牌会保留其关键帧。对于仅由 JavaScript 应用的名称,请加入 @safelist keyframes "dialog-in";。参阅动效和关键帧契约。