卡片小样
卡片底是 --zz-surface,浮起用 --zz-elevation。
主题不是编译期开关,而是一组普通自定义属性:--zz-* 定义在 :root 与[data-theme='…'] 上,切换主题只是换一个 data-theme 值。 因为它们是继承的自定义属性,任何子树都能局部换主题——一块卡片、一个侧栏,甚至下面这两个并排的面板。
三件事同时成立,才能既有全局主题又有局部主题:
| 环节 | 做法 | 为什么 |
|---|---|---|
| 定义 | token 写在 :root 与 [data-theme='dark' | 'light'] 块里 | 普通自定义属性,继承并可在任意层级被覆盖 |
| 消费 | 组件只写 var(--zz-*),不写 --color-zz-*,不写死色值 | --color-zz-* 是 @theme inline 桥接出的 Tailwind 命名空间,值被内联进工具类,不保证作为运行时变量存在 |
| 作用域 | 任何元素加 data-theme,其子树立即用新主题渲染 | 局部主题与全局主题走同一条路径,没有第二套机制 |
<!-- 深色页面里嵌一块亮色区域:不加类、不加脚本 -->
<article data-theme="light">
<h3>这张卡片是亮色的</h3>
<p>里面的按钮、输入框、描边全部自动跟随。</p>
</article>别忘了 color-scheme:主题块里跟着写 color-scheme: dark / light, 原生控件(滚动条、输入框、日期选择器)才会跟着局部主题走。
两块内容完全相同,只有 data-theme 不同。它们的样式只依赖 token, 所以局部主题在这里是真实生效的,不是截图替换。
data-theme='dark'
这段正文的颜色来自 --zz-text-secondary;改容器的 data-theme,它整体换色。
卡片底是 --zz-surface,浮起用 --zz-elevation。
data-theme='light'
这段正文的颜色来自 --zz-text-secondary;改容器的 data-theme,它整体换色。
卡片底是 --zz-surface,浮起用 --zz-elevation。
<!-- 同一批组件、同一份标记,只有容器的 data-theme 不同 -->
<div data-theme="dark">
<Button>主操作</Button>
<Button variant="outline">次要描边</Button>
<Badge variant="brand">stable</Badge>
<Input placeholder="输入框 · 控件描边" />
<Card title="卡片小样" description="…" meta="2026-10-15 · 1,204 字" />
</div>
<div data-theme="light">
<!-- 完全相同的标记 -->
</div>注意两边的描边与"浮起"并不相同:深色面板靠一圈 1px 外描边区分层级,亮色面板叠了极轻的投影。 这是 token 值的差异,不是组件的差异——两块面板里放的是同一批组件(Button / Badge / Input / Card), 没有任何一行按主题分支的样式。
全局切换就是改根元素的属性并记住选择。用它自己的过渡,别让每个颜色各自补间。
// 切换 + 记住选择
const next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'
document.documentElement.dataset.theme = next
localStorage.setItem('zz-theme', next)
// 想让切换走那道斜切裂缝(并在切换瞬间关掉全局过渡)
document.documentElement.classList.add('zz-no-transitions')
const transition = document.startViewTransition(() => {
document.documentElement.dataset.theme = next
})
transition.finished.finally(() => document.documentElement.classList.remove('zz-no-transitions'))首屏不能等 JS 跑完再决定主题,否则会闪一下默认主题。把决定提前到 <head> 里的内联脚本, 在任何样式内容绘制之前写 data-theme:
<!-- <head> 内,必须是 is:inline,且不依赖任何外部脚本 -->
<script is:inline>
;(() => {
const stored = localStorage.getItem('zz-theme')
const system = matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'
document.documentElement.dataset.theme =
stored === 'light' || stored === 'dark' ? stored : system
})()
</script><head>,且是内联的:外链脚本会晚一步执行,闪白就回来了。prefers-reduced-motion: reduce 时跳过 startViewTransition,直接同步切换。复制 [data-theme='light'] 整块,改选择器名,然后按下表逐项决定。总共有12 个颜色值 + field 六阶 + 3 个覆盖 token,再加两行层级与一行 color-scheme。
| 分组 | token | 决定什么 |
|---|---|---|
| 面 4 | --zz-bg-deep --zz-bg --zz-surface --zz-surface-2 | 凹槽 → 页面 → 面板 → 悬停面,必须逐级可区分 |
| 线 2 | --zz-border-subtle --zz-border-strong | 发丝线(常与 surface-2 同值)与控件描边;后者必须明显看得见 |
| 文字 3 | --zz-text --zz-text-secondary --zz-text-muted | 三档实色,各自独立对背景校对比度,不用透明度派生 |
| 品牌 3 | --zz-brand --zz-brand-soft --zz-brand-ink | 强调色、它的 12% 透明版、填充之上的文字色 |
| field 六阶 | --zz-field-bg --zz-field-dim --zz-field-mid --zz-field-lit --zz-field-hover --zz-field-crest | 品牌明度梯:字标渐变、像素场、滚动条滑块的 hover / active |
| 覆盖 3 | --zz-img-outline --zz-scroll-thumb --zz-selection | 图片 1px 描边、滚动条滑块、文本选区底色 |
| 层级 2 | --zz-elevation --zz-elevation-hover | 亮色主题叠真实投影,深色主题只用零模糊描边 |
[data-theme='ocean'] {
color-scheme: dark; /* 决定原生控件与滚动条的明暗 */
/* 面 4 */
--zz-bg-deep: …;
--zz-bg: …;
--zz-surface: …;
--zz-surface-2: …;
/* 线 2 */
--zz-border-subtle: …; /* 发丝线,通常与 surface-2 同值 */
--zz-border-strong: …; /* 控件描边 */
/* 文字 3 */
--zz-text: …;
--zz-text-secondary: …;
--zz-text-muted: …;
/* 品牌 3:soft 保持派生,不用手改 */
--zz-brand: …;
--zz-brand-soft: color-mix(in srgb, var(--zz-brand) 12%, transparent);
--zz-brand-ink: …;
/* field 六阶 */
--zz-field-bg: …;
--zz-field-dim: …;
--zz-field-mid: …;
--zz-field-lit: …;
--zz-field-hover: …;
--zz-field-crest: …;
/* 覆盖 3 */
--zz-img-outline: …;
--zz-scroll-thumb: …;
--zz-selection: …;
/* 层级 2:深色用 1px 零模糊描边,亮色才叠投影 */
--zz-elevation: 0 0 0 1px oklch(1 0 0 / 0.07);
--zz-elevation-hover: 0 0 0 1px oklch(1 0 0 / 0.13);
}--zz-brand-soft 保持 color-mix 派生行不动,实际只需手写 11 个颜色值。document.documentElement.dataset.theme = 'ocean',或给任意子树加 data-theme="ocean"。node scripts/check-tokens.mjs,它只允许色值出现在 packages/design-system/src/styles/tokens.css。--zz-bg 与 --zz-surface 上都看得见、正文在两种主题下都能读。