Web前端之样式中的prefers-color-scheme,一套完整的主题系统设计与原理解析

Web前端之样式中的prefers-color-scheme,一套完整的主题系统设计与原理解析

  • [什么是 prefers-color-scheme?](#什么是 prefers-color-scheme?)
  • [prefers-color-scheme 的底层工作机制](#prefers-color-scheme 的底层工作机制)
  • [prefers-color-scheme 切图片案例](#prefers-color-scheme 切图片案例)
  • [light-dark() 的真实执行模型](#light-dark() 的真实执行模型)
  • [light-dark() 经典案例](#light-dark() 经典案例)
  • [prefers-color-scheme 与 light-dark() 的区别](#prefers-color-scheme 与 light-dark() 的区别)
  • [color-scheme 才是幕后真正控制者](#color-scheme 才是幕后真正控制者)
  • [prefers-color-scheme 、light-dark() 和 color-scheme 三者分别是什么?](#prefers-color-scheme 、light-dark() 和 color-scheme 三者分别是什么?)
  • 主题切换到底在解决什么?
  • 推荐架构(核心思想)
  • 为什么必须这样分层?
  • [light-dark() 在架构中的正确定位](#light-dark() 在架构中的正确定位)
  • 最终推荐组合(最佳实践)

什么是 prefers-color-scheme?

prefers-color-scheme是 CSS 的媒体特性(Media Feature),用来判断操作系统或浏览器当前的深浅色模式:
系统深色 => dark
系统浅色 => light
它的核心作用是 条件规则切换,可以控制整段 CSS 生效,而不仅仅是单个属性值。


基本用法

css 复制代码
@media (prefers-color-scheme: dark) {
  .card {
    background-color: #111111;
    color: #ffffff;
  }
}

解释
如果系统是深色模式,则上面的规则生效。
如果系统是浅色模式,则规则不生效,card 会使用默认样式。


核心特点
规则级:可以替换整段 CSS,包括图片、布局、边框等。
自动监听系统主题:不需要 JS。
不响应点击:只能响应操作系统主题变化。


prefers-color-scheme 的底层工作机制

它到底"监听"的是什么?
很多文章会说:"监听系统主题",但这不严谨。
更准确的说法是:浏览器在渲染阶段,向 CSS 暴露了一个 environment media feature(环境媒体特性)。

css 复制代码
(prefers-color-scheme: dark)

这个值来源于:操作系统主题 => 浏览器 UI 主题 => 渲染引擎 => CSS 媒体查询
关键点:1、CSS 本身 没有能力读取系统;2、是浏览器把这个信息"注入"到 CSS 环境中


它什么时候触发?
不是"初始化一次",而是动态响应。
当切换系统主题时:

bash 复制代码
OS主题变化
↓
浏览器收到事件
↓
重新计算媒体查询(media query evaluation)
↓
触发样式重算(recalc style)
↓
重新绘制(repaint)

注意:1、不会重新布局(layout)(除非你改了布局属性);2、只是 style + paint 层变化


为什么它是"规则级"能力?

css 复制代码
.card {
  background-image: A;
}

@media (prefers-color-scheme: dark) {
  .card {
    background-image: B;
  }
}

本质不是"修改值",而是:条件性地激活另一组 CSS 规则
浏览器内部等价于

javascript 复制代码
if (dark) {
  使用规则集 B
} else {
  使用规则集 A
}

所以它可以做到:
1、换图片 ✔
2、换布局 ✔
3、换字体 ✔
4、换动画 ✔
这就是它和light-dark()的第一层本质差异。


prefers-color-scheme 切图片案例

html

html 复制代码
<div class="card">
    <p>切换系统深浅色,观察背景图变化</p>
</div>

style

css 复制代码
* {
    margin: 0px;
    padding: 0px;
    box-sizing: border-box;
    font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
}

body {
    width: 100vw;
    height: 100vh;
    display: grid;
    place-items: center;
}

.card {
    width: 368px;
    height: 268px;
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 8px;
    background-image: url("https://picsum.photos/368/268?random=101");
    background-size: cover;
    background-position: center;
    border-radius: 18px;
    text-shadow: 0 2px 6px rgba(0, 0, 0, .6);

    p {
        color: #fff;
        font-size: 18px;
    }
}

/* 系统深色模式自动换图 */
@media (prefers-color-scheme: dark) {
    .card {
        background-image: url("https://picsum.photos/368/268?random=202");
    }
}

解析
1、prefers-color-scheme是规则级,可以替换整段 CSS。
2、切换系统主题 => 背景图直接变。
3、light-dark()无法做到图片切换,因为它只能返回属性值。


light-dark() 的真实执行模型

light-dark() 到底在做什么?
很多人理解成:"自动切换颜色"
但真实模型是:在"计算值阶段(computed value stage)"做一次条件选择

css 复制代码
color: light-dark(black, white);

浏览器执行流程:

bash 复制代码
解析 CSS
↓
计算属性值(computed value)
↓
根据当前 used color scheme 选择 black 或 white
↓
得到最终值

注意关键词
1、它发生在 值计算阶段
2、不是规则切换
3、不会生成新的 CSS 规则


为什么它不能切图片?
因为 CSS 有严格的类型系统

类型 示例
<color> #fff
<image> url(...)
<length> 10px

light-dark()的签名是:light-dark(<color>, <color>);,它只接受 <color> 类型
所以:background-image: light-dark(url(a), url(b));,类型不匹配 => 直接无效


light-dark() 经典案例

html

html 复制代码
<div class="card">
    颜色会根据系统主题自动切换
</div>

style

css 复制代码
* {
    margin: 0px;
    padding: 0px;
    box-sizing: border-box;
    font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
}

:root {
    /* 必须声明 */
    color-scheme: light dark;
}

body {
    width: 100vw;
    height: 100vh;
    display: grid;
    place-items: center;
    background: #00ff00;
}

.card {
    width: 368px;
    height: 268px;
    padding: 8px;
    text-align: center;
    line-height: 268px;
    /* 值级切换,颜色跟随系统主题 */
    background: light-dark(#ffffff, #111111);
    color: light-dark(#111111, #ffffff);
    border-radius: 18px;
    text-shadow: 0 2px 6px rgba(255, 255, 255, .6);
    transition: .3s;
}

prefers-color-scheme 与 light-dark() 的区别

特性 prefers-color-scheme light-dark()
作用层级 规则级(整段 CSS) 值级(单个属性)
可切换类型 图片、布局、颜色、资源路径 颜色、数值、单位
响应触发 系统深浅色切换 系统深浅色切换
是否可点击触发
代码复杂度 中等 极简

一句话理解
颜色 => light-dark()
资源 / 布局 / 整段规则 => prefers-color-scheme
这也是现代 CSS 主题系统的标准分工。


color-scheme 才是幕后真正控制者

代码里真正发生的事情

css 复制代码
:root {
    color-scheme: light dark;
    --bg: light-dark(#ffffff, #111111);
}

这里其实做了三件事
1、声明页面支持两种主题

css 复制代码
color-scheme: light dark;

作用是告诉浏览器,可以使用深色 UI 或可以使用浅色 UI
否则,light-dark()可能不会按预期工作或表单、滚动条不会自动变暗


2、定义"依赖环境"的变量

css 复制代码
--bg: light-dark(#ffffff, #111111);

这个变量不是固定值,而是:一个依赖环境的动态计算值


3、变量在"使用时"才被解析

css 复制代码
.card {
  background: var(--bg);
}

执行顺序

bash 复制代码
读取 var(--bg)
↓
展开为 light-dark(...)
↓
根据当前 color-scheme 选择值
↓
得到最终颜色

prefers-color-scheme 、light-dark() 和 color-scheme 三者分别是什么?

bash 复制代码
prefers-color-scheme 是在"规则层"做条件分支

light-dark() 是在"值计算阶段"做条件选择

color-scheme 是决定整个页面"使用哪种主题上下文"的根控制器

主题切换到底在解决什么?

很多人理解为:深色 / 浅色 切换
但工程上真正的问题是:如何在不同"视觉语义"下,统一管理设计变量
关键词:
1、不是"颜色切换"
2、是 Design Token(设计令牌)切换


推荐架构(核心思想)

现代主题系统应该分三层

bash 复制代码
设计语义层(Design Token)
主题环境层(Theme Context)
表现层(Component Style)

1、设计语义层(最关键)
错误写法

css 复制代码
color: #111111;
background: #ffffff;

正确写法

css 复制代码
:root {
  --color-text: light-dark(#111111, #ffffff);
  --color-bg: light-dark(#ffffff, #111111);
  --color-border: light-dark(#e5e7eb, #374151);
}

这里发生了质变:1、不再关心"黑或白";2、只关心"语义"


主题环境层(prefers-color-scheme 的职责)
这一层只做一件事:决定当前是 light 还是 dark

css 复制代码
@media (prefers-color-scheme: dark) {
  :root {
    --elevation-shadow: 0 4px 12px rgba(0,0,0,.6);
  }
}

注意:
这里适合处理:
1、阴影
2、图片
3、特殊效果
不适合写大量颜色(否则会失控)


表现层(组件使用)

css 复制代码
.card {
  background: var(--color-bg);
  color: var(--color-text);
  border: 1px solid var(--color-border);
}

组件层永远不关心:现在是 dark 还是 light
它只关心:我用什么语义变量


为什么必须这样分层?

因为这能解决三个"工程级问题":可维护性、可扩展性(未来不止 dark/light)和跨端统一。


可维护性

css 复制代码
@media (prefers-color-scheme: dark) {
  .card { ... }
  .btn { ... }
  .header { ... }
}

问题:
1、每个组件都要写一遍
2、主题逻辑分散


可扩展性(未来不止 dark/light)
未来可能有:dark、light、dim、oled或high-contrast
如果用的是:light-dark()直接扩展失败


跨端统一
做的可能是:Web、小程序、App(Flutter / Swift)
Design Token 可以:一套变量 => 多端复用


light-dark() 在架构中的正确定位

很多人误用它。正确理解是:它只是"默认 token 的快速写法"


适合场景

css 复制代码
:root {
  --color-text: light-dark(#111111, #ffffff);
}

优点
1、写法简洁
2、无需 media query


不适合场景

css 复制代码
background-image: light-dark(...)
css 复制代码
layout: light-dark(...)

原因
1、它不是规则系统
2、只是值选择器


最终推荐组合(最佳实践)

基础层(color-scheme)

css 复制代码
:root {
  color-scheme: light dark;
}

Token 层(light-dark)

css 复制代码
:root {
  --color-bg: light-dark(#ffffff, #111111);
  --color-text: light-dark(#111111, #ffffff);
}

规则层(prefers-color-scheme)

css 复制代码
@media (prefers-color-scheme: dark) {
  :root {
    --bg-image: url(dark.jpg);
  }
}

使用层(组件)

css 复制代码
.card {
  background: var(--color-bg);
  color: var(--color-text);
}

最终认知升级(关键一句话)

bash 复制代码
prefers-color-scheme 决定"规则走哪一套"

light-dark() 决定"值选哪一个"

Design Token 决定"系统如何组织"
相关推荐
Jinuss2 小时前
源码分析之React中的useId
前端·javascript·react.js
后海大草鱼2 小时前
PTE考试谁说RS必须全对?Repeat Sentence提分从0到会就看这篇
前端·后端
前端缘梦2 小时前
Next.js 实现AI流式输出(打字机效果)
前端·面试·全栈
oscar9992 小时前
给 Claude Code 装上浏览器:Chrome 集成测试版详解
前端·chrome·集成测试·浏览器
美团内卖2 小时前
雪碧图还在手写 background-position?试试这款 Vite + Vue3 构建期雪碧图插件
前端
大萝卜呼呼2 小时前
Next.js第三课 - 布局与页面 - 优栈
前端·next.js
码云数智-园园2 小时前
Java接口与抽象类:从设计哲学到应用场景的深度辨析
前端
莱昂晨2 小时前
Vue 3偶发字体乱码 - 原因探究
前端·javascript·vue.js
AlkaidSTART2 小时前
0 基础入门 Zustand:新手友好的 React 状态管理方案
前端·javascript