网站导航

设计基础

颜色

以自适应角色构建界面颜色,结合圆角与阴影突出表面,并选择明暗模式。

本页内容

#概览

以角色构建界面颜色:表面、线条与文本的 class 在明暗模式中保持不变。当颜色本身就是重点,例如插图或品牌点缀时,再选用色阶。颜色变化应对应角色、状态,或读者需要辨识的差异。

模式改变,角色不变

只需选择一次角色。它的 light-dark() 值会跟随元素配色方案,包括嵌在另一配色中的区域。

文字说明
  1. 保留角色. 两个区域使用相同的表面、分隔线、强调文本与次要文本 class。
  2. 按区域解析. 角色值使用 light-dark()。每个区域的 color-scheme 会选择亮色或暗色,无需改变 class。

#选择角色或色阶

需求使用示例
页面结构与文本层级颜色角色bg-surface-raised, b-line-divider, fg-text-muted
特定颜色色阶bg-blue-5, fg-blue-60

同一个布局,两种模式

浅色
深色
两个文档使用完全相同的 class。当前模式提供表面、线条与文本的值。
<section class="p-md r-lg b(1) b-solid b-line-divider bg-surface-raised fg-text-body">  <h2 class="m(0) text-lg font-regular fg-text-strong">Project updates</h2>  <p class="mt-xs mb-md text-sm fg-text-muted">Three milestones changed this week.</p>  <a class="fg-text-link underline" href="/updates">Continue</a></section>
生成的 CSS
@layer theme {  :root,  :host {    --color-zinc-40: oklch(68.6% .0028 265);    --color-zinc-60: oklch(48.6% .003 265);    --color-zinc-90: oklch(24.6% .0022 265)  }  :root,  :host {    --color-surface-raised: light-dark(var(--color-white), var(--color-zinc-90));    --color-line-divider: light-dark(oklch(0% 0 none / .12), oklch(100% 0 none / .12));    --color-text-muted: light-dark(var(--color-zinc-60), var(--color-zinc-40))  }  :root,  :host {    --color-white: oklch(100% 0 none)  }}@layer utilities {  .bg-surface-raised {    background-color: var(--color-surface-raised)  }  .b-line-divider {    border-color: var(--color-line-divider)  }  .fg-text-muted {    color: var(--color-text-muted)  }}

角色是颜色键名的一部分:bg-surface-raised 读取 --color-surface-raised,b-line-divider 读取 --color-line-divider。线条角色只设置颜色,因此边框宽度与样式须另外指定。颜色角色列出所有角色,调色板则列出完整色阶。

边框颜色需要宽度与样式配合

所有方框初始均为 border-style: none。示例的 border-box 尺寸相同,仅改变所列边框声明。

文字说明
  1. b-line-divider 提供 border-color,b(2) 提供 border-width。两者均不改变 none 的边框样式。
  2. 加入 b-solid 后才会绘制 2px 边框。边框可沿用组件中其他已编写的必要条件,因此仅看到颜色类时请检查完整样式。

区分固定色阶与语义角色

特定颜色使用色阶;需要随场景调整的决策则使用角色。

文字说明

两条路径分别呈现固定色阶,以及选择明暗值的角色。

#固定与自适应色相别名

fg-yellow 这类色相别名是固定颜色,在每个模式中都使用相同的值。fg-text-yellow 这类文本色相别名则会按模式选择适合文本的前景色阶。请在实际背景上检查对比度;自适应别名无法保证每种搭配都合适。

<svg class="fg-yellow" fill="currentColor" aria-hidden="true">…</svg><p class="fg-text-yellow">Pending review</p>
生成的 CSS
@layer theme {  :root,  :host {    --color-yellow-40: oklch(91.5% .1909 101.1);    --color-yellow-90: oklch(52.17% .1199 62.2)  }  :root,  :host {    --color-yellow: var(--color-yellow-40);    --color-text-yellow: light-dark(var(--color-yellow-90), var(--color-yellow-40))  }}@layer utilities {  .fg-text-yellow {    color: var(--color-text-yellow)  }  .fg-yellow {    color: var(--color-yellow)  }}

彩色标签使用文本色相别名;正文、强调文本与链接则使用文本角色。


#凸起表面

凸起表面结合表面角色、圆角 token 与阴影 token。可交互的表面在悬停或获得焦点时,将阴影提高一级。普通卡片与控件使用小圆角;pill 留给刻意完全圆润的形状,例如下方徽章。

凸起表面

浅色
深色
两份文档使用相同 class。表面、文本与阴影颜色由当前模式提供;阴影几何形状保持不变。
<a class="block p-md r-lg bg-surface-raised fg-text-body shadow-sm shadow-md:hover shadow-md:focus-visible" href="/notes">  <span class="flex items-center justify-between gap-sm">    <span class="fg-text-strong">Field notes</span>    <span class="px-xs py-3xs pill bg-line-subtle text-xs fg-text-muted">12 new</span>  </span></a>
生成的 CSS
@layer theme {  :root,  :host {    --radius-lg: .5rem  }  :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));    --shadow-md: 0 0 0 1px light-dark(oklch(0% 0 none / .05), oklch(100% 0 none / .06)), 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)), 0 8px 8px -4px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .26))  }}@layer utilities {  .r-lg {    border-radius: var(--radius-lg)  }  .pill {    border-radius: calc(infinity * 1px)  }  .shadow-sm {    box-shadow: var(--shadow-sm)  }  .shadow-md\:hover:hover {    box-shadow: var(--shadow-md)  }}

预设阴影用 light-dark() 设置颜色,因此 shadow-sm 保留几何形状,并跟随元素的配色方案。pill 是固定工具类,不是圆角 token;宽高相等时会形成圆形。使用 rtl-lg、rtr-lg、rbl-lg 与 rbr-lg 可分别设置单个角。各种尺度与自定义方式请见圆角与阴影。

相同 pill 圆角随方框形状调整

高度保持 64px。pill 提供圆角半径,示例尺寸来自独立尺寸声明。

文字说明
  1. pill 设置全圆角半径,不设置宽度、高度或内边距。
  2. 宽高相等时形成圆形。同高但较宽的方框形成胶囊,因此仅靠类并不保证圆形。

#明暗模式

预设在根元素设置 color-scheme: light dark,角色与阴影则使用 light-dark(),因此页面无需额外 class 就会跟随系统偏好。若要指定模式,在根元素、嵌套区域或 Shadow Host 上设置 data-theme="light" 或 data-theme="dark":

一个 token,两种局部配色

@theme {  --color-surface-notes: light-dark(#f4f4f5, #27272a);}@layer base {  [data-theme="light"] {    color-scheme: light;  }  [data-theme="dark"] {    color-scheme: dark;  }}
生成的 CSS
@layer base {  [data-theme="light"] {    color-scheme: light;  }  [data-theme="dark"] {    color-scheme: dark;  }}@layer theme {  :root,  :host {    --color-zinc-30: oklch(78% .0025 265);    --color-zinc-70: oklch(39.8% .0028 265)  }  :root,  :host {    --color-text-body: light-dark(var(--color-zinc-70), var(--color-zinc-30))  }  :root,  :host {    --font-weight-regular: 400;    --font-size-sm: .875rem;    --font-size-md: 1rem;    --font-size-lg: 1.125rem;    --text-sm: var(--font-size-sm);    --text-sm--line-height: 1.5;    --text-sm--letter-spacing: 0;    --text-md: var(--font-size-md);    --text-md--line-height: 1.6;    --text-md--letter-spacing: 0;    --text-lg: var(--font-size-lg);    --text-lg--line-height: 1.5;    --text-lg--letter-spacing: 0;    --radius-md: .375rem;    --radius-lg: .5rem;    --spacing-xs: .5rem;    --spacing-md: 1rem  }  :root,  :host {    --color-surface-notes: light-dark(#f4f4f5, #27272a)  }}@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-md {    font-size: var(--text-md);    line-height: var(--text-md--line-height, normal);    letter-spacing: var(--text-md--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-notes {    background-color: var(--color-surface-notes)  }  .r-lg {    border-radius: var(--radius-lg)  }  .r-md {    border-radius: var(--radius-md)  }  .fg-text-body {    color: var(--color-text-body)  }  .font-regular {    font-weight: var(--font-weight-regular)  }  .m\(0\) {    margin: calc(0 * 1px)  }  .mb-md {    margin-bottom: var(--spacing-md)  }  .mt-xs {    margin-top: var(--spacing-xs)  }  .mb\(0\) {    margin-bottom: calc(0 * 1px)  }  .wrap-anywhere {    overflow-wrap: anywhere  }  .p-md {    padding: var(--spacing-md)  }}
两个区域都使用 bg-surface-notes。data-theme 属性选择不同配色,因此 light-dark() 提供暗色外层表面与亮色内层表面。

预设已内置这两个 data-theme 选择器;示例 CSS 再次列出它们,以说明启用方式。用 light-dark() 定义的自定义颜色也会以相同方式调整,切换模式无需重新编译。嵌套区域会继承父级 token,除非自行重新声明;品牌主题应使用自己的选择器,例如 [data-brand="ocean"],并在其中重新声明角色 token。

@light 与 @dark 后缀是 prefers-color-scheme 媒体查询。手动设置 data-theme 会改变 token,但不会启用这些查询。若 class 必须响应手动选择,请声明选择器 mixin 并应用:

index.css
@mixin --manual-dark {  &:where([data-theme="dark"], [data-theme="dark"] *) { @contents; }}
<img class="(opacity:.85)@apply(--manual-dark)" src="/chart.png" alt="Weekly usage" />

这个 mixin 检查的是属性,而不是实际使用的配色方案,因此在嵌套的亮色区域内也会匹配。优先使用自适应 token;作用范围与解析方式请见变量与模式。

祖先选择器可以进入浅色区域

虚线路径追踪显式 dark 属性。内层浅色边界改变 color-scheme,不改变选择器检查的祖先关系。

文字说明
  1. 本文的 --manual-dark 混入匹配 [data-theme="dark"] 本身或其后代元素。
  2. 嵌套 data-theme="light" 区域中的图表仍是外层 dark 区域的后代。因此即使 light-dark() 值使用本地浅色配色,其不透明度包装仍然匹配。
  3. 需要跟随实际采用配色的值请使用自适应令牌;属性选择器仅检查其显式指定的关系。

最近的配色方案决定颜色

嵌套边界代表 color-scheme。Token 名称与标记保持不变。

文字说明

亮色文档包住一个暗色区域。同一个语义 token 在外侧绘制亮色表面,在区域内绘制暗色表面。

#在实际场景中检查角色

在两种配色中检查相同内容。确认强调与次要文本和实际表面的对比度、链接的普通与焦点状态,以及用来识别控件的边界。颜色 token 提供一个值,本身不会建立对比度、确保焦点可见,或赋予状态含义。

选中项或状态应搭配标签、图标或其他可见提示,避免只靠颜色。嵌套区域须单独验证:深色页面中的手动亮色区域,仍需有适当且易读的表面与前景搭配。