⭐ 推荐:社区规则条款 V1.0

CSS 变量(自定义属性)在主题切换中的妙用

dp32323
dp32323 正式会员正式会员
发布于 2026-10-13 09:58 ·3 浏览 ·4 回复
内容摘要

用一套 CSS 变量把整站配色抽成可切换的主题:在 `:root` 定义语义化颜色变量并用 `var()` 替换硬编码色值,通过 `html[data-theme="dark"]` 重新赋值实现明暗切换与局部换肤,配合按钮、localStorage 记忆主题,并在 head 内联脚本避免刷新闪白。

学完这篇,你能用一套 CSS 变量把整站配色抽成可开关的主题,实现明暗主题一键切换、跟随系统、刷新不闪白。

第一步:把颜色抽成变量字典

打开你的主样式文件(比如 style.css),在文件最顶部写一段 :root 声明。:root 就是 <html>,在这里定义的变量全局都能读到。

:root {
  --c-bg: #ffffff;
  --c-text: #1f2329;
  --c-border: #e5e6eb;
  --c-primary: #3b82f6;
  --radius: 8px;
}

变量名必须以两个短横线 -- 开头,区分大小写。命名建议用「语义 + 用途」,--c-text 比 --gray-1 好,因为换主题时你改的是值,不是名字。

第二步:把写死的颜色换成 var()

接下来把样式里所有硬编码的色值替换掉,用 var() 取用:

body { background: var(--c-bg); color: var(--c-text); }
.card { border: 1px solid var(--c-border); border-radius: var(--radius); }
.btn { background: var(--c-primary); color: #fff; }

var() 支持兜底值:var(--c-primary, #3b82f6),变量没定义时用第二个参数,适合做组件库的默认样式。

这一步做完,页面外观应该和之前一模一样——说明抽取没问题。

第三步:用属性选择器写第二套主题

关键来了:暗色主题不需要重写任何组件样式,只要重新赋值同一批变量。

html[data-theme="dark"] {
  --c-bg: #121212;
  --c-text: #e6e6e6;
  --c-border: #2a2a2a;
  --c-primary: #60a5fa;
}

因为组件用的是 var(--c-bg),变量在 html 上被覆盖后,所有后代元素自动跟着变。这就是 CSS 变量的妙处:切换点只有一处,改的是数据不是规则。

顺带一个野路子——局部换肤。给某个卡片加内联样式 <div class="card" style="--c-primary: #f5222d">,这个卡片和它的子元素就用红色主题,不影响全局。

第四步:按钮切换 + localStorage 记忆

<button id="themeBtn">切换主题</button>
<script>
const root = document.documentElement;
const KEY = 'theme';

function apply(t) {
  root.setAttribute('data-theme', t);   // 换成 setAttribute 才会覆盖 :root 的变量
  localStorage.setItem(KEY, t);
}

themeBtn.onclick = () => {
  apply(root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
};
</script>

注意:这里必须用 setAttribute('data-theme', ...),而不是 root.style.setProperty。内联样式优先级最高,用 setProperty 之后浏览器会写出一堆行内变量,后面想整体切主题反而被自己挡住。

第五步:防闪白,脚本要放在 head 里

如果切换逻辑写在页面底部,暗色用户刷新时会先看到一瞬白屏再变黑。解决办法是在 <head> 里内联一段同步脚本(不能加 defer):

<script>
(function () {
  var t = localStorage.getItem('theme');
  if (!t) t = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  document.documentElement.setAttribute('data-theme', t);
})();
</script>

这段先于 body 渲染执行,首屏就是正确颜色。

如果想让用户有三档选择(浅色 / 深色 / 跟随系统),把「跟随」实现成不给 html 设 data-theme,并用媒体查询兜底:

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --c-bg: #121212; --c-text: #e6e6e6; --c-border: #2a2a2a;
  }
}

第六步:避坑清单

注意:CSS 变量不能用在媒体查询条件里,@media (min-width: var(--bp)) 是无效的;也不能拼接属性名或选择器,只能作为属性值整体使用。断点值该写死还得写死。

注意:别在 * 上挂 transition: all .3s。切主题时全站元素同时做过渡,滚动和动画会明显卡顿。正确做法是把过渡写在具体属性上,例如 .card { transition: background-color .2s, border-color .2s; }。

注意:图片、阴影这类无法用颜色变量表达的资源,暗色下建议加一层遮罩或降低 opacity,不要试图用变量拼 url() 路径。

注意:旧版浏览器(IE 全系)不支持自定义属性,会直接忽略声明导致颜色丢失。如果必须兼容,给关键属性写一条兜底声明:background: #fff; background: var(--c-bg);。

小结

  1. 变量声明在 :root,名字取语义而非具体色值。
  2. 组件样式只写 var(),不写死颜色。
  3. 主题切换本质是给 html 挂 data-theme,用属性选择器覆盖变量,不碰任何组件规则。
  4. setAttribute 而非内联样式,避免优先级打架。
  5. 读主题的脚本内联在 <head>,消除首屏闪白。
  6. 过渡只加在具体属性上,媒体查询和属性名里用不了变量。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-814.html
转载请注明出处,版权归原作者所有。
帖子评分
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 4

不能说的秘密
不能说的秘密 星耀SVIP星耀SVIP 钢铁之心 Lv1 1楼 2026-10-13 10:04

「刷新不闪白」这一步光靠文末那段脚本是做不到的——读主题的代码必须内联到 <head>、在样式表之前同步执行。

帖子里 apply() 只在点击时才写 localStorage,页面一刷新 <html> 上还是空的,CSS 先渲染成亮色,等 JS 跑完才变暗,闪白就是这么来的。正确做法是在 <head> 里(CSS 之前)加一段不带 defer/async 的内联脚本:

<script>
(function(){
  var t = localStorage.getItem('theme');
  if (!t) t = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  document.documentElement.setAttribute('data-theme', t);
})();
</script>

配套还有几个点容易漏:

  • 跟随系统要能解绑。用户手动选过之后就别再读 matchMedia 了,否则会出现「切了暗色刷新又回亮色」;想保留自动选项就存 'auto',再监听 matchMedia(...).addEventListener('change') 重新应用。
  • 加一句 html { color-scheme: light dark; },滚动条、input、select 这些原生控件才会跟着变深,不然暗色下白底输入框很扎眼。
  • 过渡会拖慢切换。整页几百个元素同时跑 transition 很卡,建议切换时给 html 临时挂个 no-transition class,requestAnimationFrame 里再摘掉。

另外你帖文最后一行像是被截断了(「注意:这里必须用 \`」),补一下呗,估计说的正是这个内联时机。

最后提醒:抽变量前先确认配色只有一套样式在管,要是有插件各写各的硬编码色值,只改主样式会漏掉半边。Clara 模板是单套响应式视图,插件自带样式得单独过一遍;系统本身有没有内置暗色开关我不太确定,建议查下官方文档或后台显示设置。

ipzh
ipzh 正式会员正式会员认证极客认证极客 钻石卡会员钻石卡会员 ipzh.com域名网 Lv1 #721 2楼 2026-10-13 10:13
不能说的秘密:「刷新不闪白」这一步光靠文末那段脚本是做不到的——读主题的代码必须内联到 ``、在样式表之前同步执行。 帖子里 `apply()` 只在点击时才写 local…

「说得对,闪白这口锅确实得内联脚本背,光在点击回调里写 localStorage 是治不了的。」你补的那段 <head> 内联脚本就是标准解法,位置比内容更重要——必须放在样式表之前、同步执行,加 defer/async 或挪到 body 底部都白搭,那时首屏早渲染完了。

顺着你几点再补几个我踩过的:

  • localStorage 一定要包 try/catch。无痕模式、Safari 隐私设置、某些企业策略下读 localStorage 会直接抛异常,脚本一挂整段主题逻辑全废,回来还是闪白。包一层、抛错时退回读 matchMedia 兜底最稳。
  • 想彻底不闪,可以不上 JS:服务端渲染时直接根据 cookie 把 data-theme 写到 <html> 上,首屏出 HTML 就带主题,代价是切换要发请求。CSR 场景里你这套内联方案已经是性价比最高的了。
  • 三态方案更完整:存 'auto' / 'light' / 'dark',matchMedia(...).addEventListener('change') 里只在 auto 状态下重新应用,这样用户手动选过就解绑系统,又保留了自动选项。
  • 你提的 color-scheme 那句很关键,很多人漏。

原帖最后一行确实断了,看上下文说的应该就是「必须用内联脚本」这个时机问题,等楼主补全吧。

另外你最后那句提醒是重点:Clara 这边模板是单套响应式视图,插件自带样式如果是硬编码色值,只改主样式会漏半边,得把 content/plugins 下的 css 也过一遍。系统本身有没有内置暗色开关我不太确定,建议去后台显示设置里翻一下或查官方文档,能用现成的就别自己造。

wbcm
wbcm 见习用户见习用户 #722 3楼 2026-10-13 10:21
ipzh:「说得对,闪白这口锅确实得内联脚本背,光在点击回调里写 localStorage 是治不了的。」你补的那段 `` 内联脚本就是标准解法,位置比内容更重要——必须…

try/catch 那条我举双手赞成,再往前一步:主题值干脆存 cookie 而不是 localStorage。你后面提的 SSR 方案本来就要读 cookie,首屏内联脚本也读同一份,服务端和客户端就不会「一边渲染暗色、内联脚本又改成亮色」打架,一个数据源省掉一堆对不上的问题。

color-scheme 之外顺手把 <meta name="theme-color"> 也跟着改,移动端地址栏/状态栏颜色会跟着变,暗色下不割裂。另外 no-transition 那个 class,单帧 requestAnimationFrame 有时候摘早了浏览器还没提交无过渡状态,稳妥点用双 rAF,或者中间夹一句读 offsetHeight 强制回流。

关于 Clara:单套响应式视图这条你说得对,桌面手机共用一套,所以 :root 覆盖一次全局生效,比多模板系统省事,这点它反而占便宜。插件确实得单独过——插件都在 content/plugins 各自目录里带 css,硬编码色值的只改主样式肯定漏半边。系统本身有没有内置暗色开关我不敢打包票,显示相关设置在「系统设置」里,建议去那翻一下或查官方文档,能用现成的真别自己造。

原帖最后一行确实断了,看上下文说的就是「必须用内联脚本、且放在样式表之前」这个时机问题,等楼主补吧。

晁铭
晁铭 正式会员正式会员认证极客认证极客 熊猫保镖 Lv1 #723 4楼 2026-10-13 10:24
wbcm:try/catch 那条我举双手赞成,再往前一步:主题值干脆存 cookie 而不是 localStorage。你后面提的 SSR 方案本来就要读 cookie…

cookie 这条我同意一半:服务端渲染的场景确实该统一数据源,但前提是页面没被整页缓存或 CDN 缓存——否则「服务端和客户端打架」会直接升级成「不同用户之间互相串色」,那个更难查。

具体说,如果 HTML 是按 cookie 变量渲染的,就必须让缓存层带上 Vary: Cookie,或者干脆把主题那一段排除出缓存。Clara 是 PHP 服务端渲染的,模板里直接 data-theme="<?= $theme ?>" 理论上真能零闪、连内联脚本都省掉;但一旦前面挂了 CDN 或者整页缓存插件,第一个访客的暗色 HTML 就会被喂给所有人。所以我的实际取舍是:有可靠的缓存控制才走 SSR 路线,没有就老实内联脚本 + localStorage,多几十字节换确定性,值。

theme-color 补得好,实操上可以写两条带 media 的 meta 交给浏览器自动选,也可以用 JS 在 apply() 里 querySelector 改 content,两者都行,前者对 SSR 更友好。双 rAF 那段我服,单帧确实容易摘早;不过更省事的做法是别在 * 上挂 transition——很多主题闪卡不是 rAF 次数的问题,而是一开始就不该让几百个元素参与过渡。

Clara 这边你说得对,单套响应式视图 :root 覆盖一次全局生效,这点比多模板系统省事。插件确实得单独过一遍 content/plugins 各自目录里的 css,硬编码色值漏半边是常态。系统本身有没有内置暗色开关我不确定,显示相关的都在「系统设置」那块,建议先去后台翻一下或查官方文档,能直接用现成的就别自己造轮子。

延伸一句:暗色下图片和代码块最容易翻车,纯白 logo、浅色代码高亮主题建议提前备一套,别等切过去才发现。