团队空间
设计系统更新
组件通过主题变量获得颜色,不依赖页面级选择器。
来源:17golang原创
时间:2026-10-23 13:24:56 270浏览 收藏
CSS 的 style() 查询适合把“主题由谁提供”和“组件怎样呈现”拆开:在组件外层设置可继承的 --theme,组件内部使用 @container style(--theme: dark) 查询它。主题值命中时覆盖颜色、边框和阴影;没有命中时继续使用默认样式。这样切换主题只需要改变父级变量,不必为每个组件复制一套 .dark 选择器。
这里的 style() 是容器样式查询,不是按视口宽度判断的尺寸查询。MDN 的说明明确指出,样式查询目前主要围绕 CSS 自定义属性展开,普通属性的样式查询不能直接当作尺寸查询来写。
@container style(--theme: dark) 判断的是容器的计算样式,命中后只影响查询块中的后代。主题变量是组件和页面之间的一条小契约。组件只约定 --theme 的值,页面负责决定它是 light 还是 dark。下面的 HTML 把主题作用域放在两个卡片的共同祖先上,后续只切换这个作用域的属性。
团队空间
设计系统更新
组件通过主题变量获得颜色,不依赖页面级选择器。
这里没有给 profile-card 添加 container-type。样式查询可以把普通元素作为样式容器;只有混入宽度或高度条件时,才需要为相应祖先声明 container-type: inline-size 或 size。主题变量写在作用域上,子树中的卡片就能通过继承得到它。

先写默认样式,再声明主题查询。默认规则保证旧浏览器或变量值不匹配时仍有可读的组件;查询规则只负责把深色主题的差异集中在组件内部。
.theme-scope {
--theme: light; /* 默认主题写在组件共同祖先上 */
}
.profile-card {
--card-bg: #ffffff; /* 回退值让基础样式可独立工作 */
--card-fg: #172033;
--card-muted: #526079;
--card-accent: #2358d7;
max-width: 28rem;
padding: 1.5rem;
color: var(--card-fg);
background: var(--card-bg);
border: 1px solid #d7deea;
border-radius: 1rem;
box-shadow: 0 0.75rem 2rem rgb(25 43 80 / 12%);
}
.profile-card__eyebrow {
color: var(--card-muted); /* 次要文字也要保留足够对比度 */
}
.profile-card__action {
color: #ffffff;
background: var(--card-accent);
}
@container style(--theme: dark) {
.profile-card {
--card-bg: #182338; /* 只有 dark 主题命中时覆盖变量 */
--card-fg: #f4f7ff;
--card-muted: #c1cbe0;
--card-accent: #8fb4ff;
border-color: #4e6387;
box-shadow: 0 0.75rem 2rem rgb(0 0 0 / 30%);
}
}
关键点是选择器的作用范围:@container 里的规则会应用到匹配容器的后代,而不是把查询容器本身当成一个自动变色的目标。将卡片放在主题作用域内部后,查询到祖先的 --theme: dark,卡片就能使用深色变量。若同一棵树里有多个主题作用域,最近的相关容器会影响结果,组件的主题边界也更容易局部化。
切换控件不需要给每张卡片添加和移除主题类名,只更新作用域上的 data-theme 与 --theme。下面的脚本把按钮状态同步到可访问名称,避免页面视觉变化后读屏用户不知道当前主题。
const scope = document.querySelector(".theme-scope");
const toggle = document.querySelector("[data-theme-toggle]");
toggle.addEventListener("click", () => {
// 先读取当前值,再把新主题写回同一个作用域。
const nextTheme = scope.dataset.theme === "dark" ? "light" : "dark";
scope.dataset.theme = nextTheme;
scope.style.setProperty("--theme", nextTheme); // 触发 style() 查询重新计算
toggle.setAttribute("aria-pressed", String(nextTheme === "dark"));
toggle.textContent = nextTheme === "dark" ? "切换浅色主题" : "切换深色主题";
});
生产代码中可以把主题值保存在应用状态或用户偏好里,但不要把同一状态同时散落到多个组件节点。一个明确的作用域便于局部预览、嵌入式组件和多主题页面共存。
带值的样式查询会比较自定义属性的计算值。style(--theme: dark) 不是“变量存在就算命中”,而是要求查询到的值与 dark 匹配。无值写法 style(--theme) 则表达“该自定义属性的计算值不是初始值”这类判断;两者不要混用。

| 写法 | 判断对象 | 是否需要尺寸容器 |
|---|---|---|
@container style(--theme: dark) | 祖先容器的自定义属性值 | 不需要 |
@container (width | 容器的实际宽度 | 需要 container-type |
@media (prefers-color-scheme: dark) | 用户或系统的视口偏好 | 不查询组件祖先 |
如果组件既要响应主题又要响应自身宽度,可以在一个 @container 条件中组合两类特征,但宽度那一侧对应的祖先必须确实声明尺寸容器。不要为了让主题查询生效而给所有元素设置 container-type,这会把不必要的尺寸包含带进布局。
aria-pressed,文案说明下一步动作,焦点样式不要被主题规则覆盖掉。排查“为什么没有变色”时,按这个顺序看:主题变量是否真的写在卡片祖先上;计算值是否精确为 dark;查询块里的选择器是否命中了卡片后代;是否有后出现的普通规则覆盖;最后再看当前浏览器是否支持该特性。把这几层分开,比盲目提高选择器权重更容易维护。
只做样式查询时通常不需要。普通元素可以作为样式查询容器;只有查询自身尺寸或在同一条件中加入宽度、高度等尺寸特征时,才要声明合适的 container-type。
未注册的自定义属性只要被赋值,省略值的查询就可能为真。若你要区分主题分支,应写成 style(--theme: light) 与 style(--theme: dark),明确比较值。
当前实践应优先使用自定义属性。普通 CSS 声明的样式查询属于规范演进方向,不要把 @container (max-width: 600px) 误写成普通属性样式查询;前者是尺寸查询。
CSS style query 的价值在于把主题决策留在组件可见的作用域里:父级提供变量,组件根据计算值选择样式,默认规则承担回退。先建立这条清晰的变量契约,再考虑更多主题和尺寸组合,代码会比到处复制主题类名更容易扩展。
参考:MDN Using container size and style queries、MDN CSS container queries。