系统偏好设置的那些事儿

发布于 大漠

如果你阅读过早前分享过的《CSS媒体查询新特性》和《给网站添加暗黑模式》相关的文章,就可能想到了今天这篇文章大致和大家要聊的是些什么东西。事实上也的确如此,但或许和想象的略有差异,因为在这篇文章和大家聊的并不是 CSS 的媒体查询,也不是教大家怎么给网站添加暗黑模式的主题。如果是这样的话,就有点对不起大家了,大家也会觉得我在不断的炒冷饭。接下来要和大家聊的虽然也和 CSS 有关联,但对于很多同学来说,还是很新,很有意思的东西。感兴趣的同学请继续往下阅读。

故事背景

苹果举办的 WWDC21 已经结束很长时间了。 作为一名前端开发者而言,如果你观看过的话,你应该记得 Safari 15 设计(Design for Safari 15) 这个主题。前端大神 Jen Simmons 带你了解实时文本和可访问性最佳实践等功能,探索 CSS 和表单控件最新更新,并学习如何使用 CSS 中的宽高比(aspect-ratio)属性来创建令人难以置信的 Web 应用或页面。

在视频开始没多久,你就会看到像下图这样的一个效果:

在 HTML 的 <meta> 标签中使用 nametheme-color 的元数据可以控制系统的主题颜色:

<meta name="theme-color" content="#ecd96f" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#0b3e05" media="(prefers-color-scheme: dark)" />

在此没过几天,我的偶像 Adam ArgyleTwitter 发了一个有关于动态改变 meta 标签 theme-colorcontent 值的帖子

在你的 Web 页面上添加下面这段 JavaScript 脚本:

let scheme = document.querySelector('meta[name="theme-color"]');
let hue = 0;

setInterval(() => {
    scheme.setAttribute("content", `hsl(${(hue += 5)} 50% 30%)`);
}, 50);

你在 Safari 15 上就能看到下面这样的效果:

是不是很酷!

其实早在去年介绍《CSS媒体查询新特性》和《给网站添加暗黑模式》时都有提到 <meta>标签上的theme-color 和在 :root选择器中设置color-scheme

<!-- HTML -->
<meta name="color-scheme" content="dark light">

/* CSS */
:root {
    color-scheme: dark light;
}

但并没有深入的和大家探讨这方面的技术。更没想到的是,没过多久,这个特性已经能在一些客户端上看到了。是不是很兴奋。

系统偏好设置

在开始之前,先简单地说一下系统偏好设置。

现代科技不断的在再往前推进,智能设置越来越强大,功能也越来越先进。当然也越来越人性化,能提供给用户设置选项越来越多。毕竟人和人的喜好是不同的嘛,正所谓“青菜萝卜各有所爱”。正如《CSS媒体查询新特性》文章所介绍的,用户可以在设备上根据自己的喜好做出各种不同的设置。比如在 iOS 的设备上,你就可以随着系统来改变主题:

说不定哪一天,你就能满足你的产品经理的新需求:“用户 APP 主题颜色能根据手机壳自动调整”

<meta>标签的theme-color开始

虽然自 iOS 系统给用户提供暗黑模式可选项开始,社区中就有很多关于 CSS 如何给 Web 页面添加暗黑模式主题 相关的技术讨论。

我也紧跟时代的步伐,也在小站整理了多篇有关于这方面的文章:

在这几篇文章中都提到过:

<!-- HTML -->
<meta name="color-scheme" content="dark light">

/* CSS */
:root {
    color-scheme: dark light;
}

知道根据系统来设置主题的颜色。但汗颜的是,其中的原委在写这几篇文章的时候,并没有完全搞透彻。特别是听说 Safari 15 浏览器在 macOS 和 iOS 上都支持在 <meta> 标签上设置主题颜色时,我更为兴奋。因为总算是有浏览器支持这个特性,总算是可以一探究竟了。

注意:Safari 在当前发布的 Safari 技术预览版本(Safari Technology Preview 127)中取消了对主题颜色元标签的支持。

<meta> 标签的theme-color特性和限制

<meta> 标签是 HTML 众多标签之一,它可以根据 name 指定不定的类型,和在content中指定不同的元数据内容,比如我们熟悉的:

<meta name="viewport" content="user-scalable=no">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">

换到我们今天聊的话题中来说,<meta> 的标签的 name 属性值是 theme-color,其content可以是任意一个颜色值,比如:

<meta name="theme-color" content="#4285f4">

其中:

  • theme-color:表示一个建议的颜色,用户代理应该使用这个颜色来定制页面或周围用户界面的主题色
  • content:用来具体指定一个颜色值,这个值是一个有效的 CSS的颜色值(<color>

先来看一个简单的示例:

<!-- HTML -->
<meta name="theme-color" content="#319197" id="theme__color">

// JavaSript
const themeColorMeta = document.getElementById("theme__color");
const themeColorOutput = document.getElementById("theme__color--out");
const colorInput = document.getElementById("color");

colorInput.addEventListener("change", (etv) => {
    themeColorMeta.setAttribute("content", etv.target.value);
    themeColorOutput.textContent = etv.target.value;
});

我在 Google Pixel XL(Android 10)、三星 Galaxy S8+(Android 8.0.0)测试了其效果。下面视频是 三星 Galaxy S8+ 下的效果:

或许你已经发现了,上面示例实测出来的效果,只是系统的状态栏、浏览器的导航栏主题色是 <meta>theme-colorcontent指定的颜色,但 Web页面的 body背景颜色并没有做出任何的调整:

不过 Manuel Matuzovic 在 Chrome 安装 PWA 之后,亲测效果如下图所示:

我在自己的测试设备(Google Pixel XL(Android 10)和 三星 Galaxy S8+(Android 8.0.0))安装 PWA进行测试,并未看到上图的效果。不知道是不是偶姿势不对,这里暂且不继续往下探讨原委。我们可以在 iOS 的 Safari 15 版本发布之后再验证其效果。

在《CSS的颜色值》一文中,我们可以获知,在 Web 中(特别是在 CSS 世界中),可以有很多种方式来描述一个颜色的,比如:

而且 HTML 标准规范对 theme-color 值的定义也是如此,可以是任何 CSS 颜色。为了验证是否真的如此,我们把上面的示例稍作调整:

测试了 三星 Galaxy S8+ Android 8.0.0 下 Chrome中效果:

从上面的测试结果不难发现(虽然测试的设备不多),所有支持的浏览器也支持 hsl()rgb()。这样一来,就非常有意思了,我们可以借助 JavaScript 做一些非常酷的事情。好比文章前面 Adam Argyle 展示的效果,就是每隔一定的时间,改变 hsl() 中的色相h的值,动态改变 theme-color 的颜色。

颜色的十六进制(比如#319197),rgb()(比如rgb(125, 255, 90))、hsl()(比如hsl(230, 50%, 90%))和描述颜色的关键词(redbluehotpinkgreen)都得到了很好的支持:

我们也知道,新的颜色函数语法规则,可以在rgb()hsl()直接设置透明度(类似于rgba()hsla()),而且在theme-color 的颜色值设置带有透明度的话,表现就不太一样了。在我们这款测试机上看到的效果,透明度相当于是 100%(无透明度):

实际上,它们在大多数浏览器中都被支持,但结果有可能并不一致。比如,在MacOS 的 Safari 会解释出正确的透明度值,但似乎透明色有一条黑色基线。

在示例中,还使用了transparent 这个关键词,换句话说,在theme-color 的设置了transparent,此时,在大多数浏览器中都会渲染出你期望的效果。即 所有常规的移动浏览器都不会改变颜色,并显示默认标签

但在 MacOS上的 Safari 和 Chrome Canary PWA中,标签栏变成黑色,安卓上的 PWA 会取 manifest.json 中定义的theme-color

对于lab()lch()hwb()几个描述颜色的函数,至今只有 Safari 15支持。如果这几个函数用在别的 CSS 属性他们是能工作的,比如:

.box {
    background-color: lab(29% 39 20);
    color: lch(67% 42 258);
    border-color: hwb(297 1% 38%);
}

但是用于theme-color上,目前还无法正常工作。

更为有意思的是,如果在theme-color中使用了任何新的描述颜色的函数,Safari就无法正常解析该颜色值,且会用它们自己的算法去给theme-color选色。在 Safari 中很可能使用 <body>background-color作为theme-color值。这意味着,你可能在没有明确 theme-color 情况下得到预期的效果:

示例中还向大家演示了 CSS 颜色值的另一个关键词 currentColorcurrentColor 在 CSS 颜色设置中是非常有意思的,比如:

但到目前为止,currentColor 用于theme-color中还没有任何浏览器支持它:

这可能是一个不怎么常见的用例,但我们还是希望可以将theme-color设置为<body><html>元素的当前颜色。

<style>
    body {
        color: blue;
    }
</style>

<meta name="theme-color" content="currentColor">

有关于theme-color中取currentColor无法正常工作,也有人在 Webkit 的bug中提出

如果你期望这个Bug能早点解决,可以前往这里投票或者发表你的观点。

另外,我在示例中还尝试在theme-color使用 CSS 定义属性,看是否能生效,但事实告诉我,它在任何浏览器中都还不起作用:

/* CSS */
:root {
--theme: blue;
}

<!-- HTML -->
<meta name="theme-color" content="var(--blue)">

我在逛 Twitter 的时候,发现浏览器标签栏有渐变色:

原以为,在theme-color 中使用 CSS 的渐变就能实现

<meta name="theme-color" content="linear-gradient(#00ebff, #08124a)" />

结果你可能已经想到了。是的,和你想象的一样,没有浏览器能看到这样的效果。但有人提出另一种解决方案,就是在 <meta> 标签的 theme-color 上设置页面渐变色的起始色,即:

<meta name="theme-color" content="#00ebff" />

body {
    background: linear-gradient(#00ebff, #08124a)
}

我截了渐变不同起始色和theme-color的效果:

看上去是不是很棒!

前面我们提到过,theme-color 设置像 redblue等颜色关键词是会生效的:

看上去非常完美,一点都没问题。但事实并非如此,因为我这里的测试设备毕竟有限(想测试Safari 15,但没安装成功)。不过在网上有同学测试发现,在 Safari 浏览器中,对支持的颜色范围是有所限制的,比如 theme-color 设置 red,就未得到支持:

那是因为这些颜色(比如red)会妨碍使用界面。red(红色)不工作是因为它在视觉上与标签栏中的关闭按钮的背景颜色太接近。

这个限制是 Safari 浏览器特有的,在所有其他浏览器中,任何颜色似乎都能正常工作

theme-color 和 prefers-color-scheme

在《CSS媒体查询新特性》中有介绍过 prefers-color-scheme。它是新媒体查询新特性之一,可以让开发者完全根据用户自己喜好来控制 Web 应用或页面的主题。比如《给网站添加暗黑模式》中介绍有相关技术就离不开 prefers-color-scheme。而且在介绍暗黑模式相关技术文章中,有提到过 color-scheme 这个 CSS 属性和 <meta>nametheme-color是相同的。它们都是让开发者更容易根据用户的喜好设置来控制 Web应用或页面的主题,即 允许开发者根据用户喜好设置添加特定的主题样式

来看一个简单的示例:

<!-- HTML -->
<meta name="theme-color" content="#319197" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#872e4e" media="(prefers-color-scheme: dark)">

<style>
    body {
        background: #319197;
        color: #fff;
    }

    @media (prefers-color-scheme: dark) {
        body {
            background: #872e4e;
            color: #fff;
        }
    }
</style>

<!-- HTML -->
<body>
    <h1>Dark Mode Demo</h1>

    <script>
        if ("serviceWorker" in navigator) {
            navigator.serviceWorker.register("/sw.js");
        }
    </script>
</body>

// sw.js
self.addEventListener("install", e => {
    self.skipWaiting();
});

self.addEventListener("activate", e => {
    return self.clients.claim();
});

self.addEventListener("fetch", e => {
    e.respondWith(fetch(e.request));
});

如果你需要将你的网站安装为 PWA,还需要配置webmanifest.json,该文件包括另一个颜色定义的主题:

// webmanifest.json
{
    "name": "My PWA",
    "icons": [
        {
        "src": "https://via.placeholder.com/144/00ff00",
        "sizes": "144x144",
        "type": "image/png"
        }
    ],
    "start_url": "/theme-color-darkmode.html",
    "display": "standalone",
    "background_color": "hsl(24.3, 97.4%, 54.3%)",
    "theme_color": "hsl(24.3, 97.4%, 54.3%)"
}

注意,上面的代码来自 Manuel Matuzovic 写的测试用例中的代码

下面是支持的浏览器在亮色模式(Light Mode)下的效果。不管浏览器是否支持media,它都会解释第一个<meta>的数据:

再来看暗色模式(Dark Mode)下效果。

你会发现,Canary PWA 和 Safari 支持并显示暗色。所有的移动浏览器都使用其默认的暗色标签栏样式,除了三星,它使用亮色模式。这是因为其不支持prefers-color-scheme这个媒体查询特性。

要是你只为暗色模式定义了一个主题色,在亮色模式下会发生什么呢?

<meta name="theme-color" content="#872e4e" media="(prefers-color-scheme: dark)">

正如上图所示,我一开始认为所有的移动浏览器都将会忽略媒体查询(media)属性,只使用<meta>中的暗色,而普通的 Chrome Canary 却完全忽略了整个<meta>标签,尽管它并不支持媒体属性。正如预期的那样,这两个 Canary PWA 都回退到manifest.json配置文件中定义的主题色。

更为有趣的是,尽管没有为亮色模式设置一个主题色,但 Safari 还是显示了一个主题色。这是因为在未显式提供亮色主题色情况下,Safari 会自己选择一个主题色。在这种情况下,它使用<body>的背景颜色,但它也可能使用<header>元素的背景色。

如果你想为亮色和暗色都定义一个主题色,最好的选择是同时为这两种模式定义主题色(两种颜色),并使用第一个<meta>作为不支持媒体查询(medai)的浏览器的降级主题色:

<meta name="theme-color" content="#319197" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#872e4e" media="(prefers-color-scheme: dark)">

Safari 已经证明,主题颜色在桌面浏览器上也很好用。我相信设计师和开发者会找到很多创造性的方法来使用这个元标签(<meta>),特别是使用 JavaScript来改变这个元标签的值,将会带来更多更有创意的效果。

这里有一个细节需要特别提出。正如 Jen在视频中指出的那样,如果在你的 HTML 文档中同时出现两个<meta> 来指定theme-color 的值时,浏览器会使用第一个<meta>,而忽略其他的。这意味着你必须在顺序上首先指定你想要的默认值。

<!-- 将被运用 -->
<meta name="theme-color" content="#000000">

<!-- 将被忽略 -->
<meta name="theme-color" content="#ffffff">

这和 CSS 中样式声明刚好相反,在CSS样式规则的声明中,同一属性规则取其后:

<style>
    body {
        /* 被覆盖 */
        background: #000;

        /* 被运用 */
        background: #fff;
    }
</style>

CSS 的 color-scheme

CSS 颜色调整模块 Level 1(CSS Color Adjustment Module Lvel1)规范中引入了一个新的CSS特性。即开发者可以通过CSS对用户代理的自动颜色进行控制,目的就是处理用户的偏好设置,比如暗黑模式,对比度调整或特定的主题配置方案。

规范中的 color-scheme 属性就是用来让开发者控制一个元素可以用哪些颜色方案进行渲染。这些值与用户的偏好进行协商,从而产生一个选定的色彩方案,直接影响用户界面(UI)。比如表单控件、滚动条的默认颜色,以及 CSS 系统颜色的使用值。目前,该属性支持以下值:

  • normal :表示该元素完全没有意识到颜色方案,因此该元素应用浏览器的默认颜色方案进行渲染
  • [light | dark]+ :表示该元素知道并能处理列出的颜色方案,并在它们之间表达了一个有序的偏好

注意,提供lightdark两个关键词表明第一种方案是首选(开发者),但如果用户喜欢第二种方案,也可以接受!

在这个列表中,light表示亮色方案,有浅色背景和深色前景,而dark刚好和light相反,有深色背景和浅色前景。

对于所有的元素,用一个颜色方案进行渲染应该使用该元素在所有浏览器提供的用户界面中使用的颜色与该颜色方案的意图相一致。比如,滚动条,拼写检查的下划线、表单控件等。

:root 根元素上,用一个颜色方案进行渲染必须影响画布的表面颜色(即全局背景色),colorinitial值和系统颜色的使用值,并且还应该影响视窗的滚动条。

/* 该页面同时支持深色和浅色方案,而该页面的开发者更喜欢深色。*/
:root {
    color-scheme: dark light;
}

如果 color-scheme 写在单独的 CSS 文件中(比如通过<link rel="stylesheet">引用的.css文件),需要先下载该CSS文件并进行解析。这是需要一定时间的。为了帮助用户代理能立即使用所需的颜色方案来渲染页面的背景,可以使用namecolor-scheme<meta>标签,为color-scheme提供一个值:

<meta name="color-scheme" content="dark light" />

color-scheme 和 prefers-color-scheme 相互结合

由于<meta>元标签和CSS属性(如果运用于:root{}根元素上)最终产生的效果是相同的(渲染行为相同),但还是建议在实际开发中使用<meta>标签来指定主题颜色配色方案,这样浏览器就可以更快地采用首选方案。

虽然对于绝对基线页面(Absolute Baseline Pages)来说,没有必要制定额外的CSS规则,但在一般情况下,还是应该把color-schemeprefers-color-scheme结合起来。例如,Webkit和Chrome中链接元素(<a>)专有的颜色是blue(即rgb(0, 0, 238)),它也是-webkit-link专有颜色,但其在黑色的背景上的对比度是2.23:1不符合 WCAG AA 和 WCAG AAA标准的要求

Tomayac在 Github 的 HTML 规范中开了一个在关于这方面讨论的 Issue。感兴趣可以参与讨论,这样可以早点让浏览器厂商修复这方面的问题。

其实 color-scheme 属性和 相应的<meta>标签与prefers-color-scheme相互作用,开始可能会令人感到困惑。事实上呢?它们在一起可以发挥更好的作用。最重要的一点是,color-scheme完全决定了默认的外观,而prefers-color-scheme则决定了可样式化的外观。我们使用一些简单的示例来阐述这个观点。

假设你有下面这样的一个简单页面:

<head>
    <meta name="color-scheme" content="dark light">
    <style>
        fieldset {
            background-color: gainsboro;
        }
        @media (prefers-color-scheme: dark) {
            fieldset {
                background-color: darkslategray;
            }
        }
    </style>
    </head>
    <body>
        <p>
            Lorem ipsum dolor sit amet, legere ancillae ne vis.
        </p>
        <form>
            <fieldset>
                <legend>Lorem ipsum</legend>
                <button type="button">Lorem ipsum</button>
            </fieldset>
        </form>
</body>

页面上<style>中的CSS代码,把<fieldset>元素的背景颜色设置为gainsboro,如果用户更喜欢暗色模式,则根据prefers-color-scheme媒体查询,将<fieldset>的背景颜色设置为darkslategray

通过<meta name="color-scheme" content="dark light"> 元数据的设置,页面告诉浏览器,它支持深色(dark)和亮色(light)主题,并且优先选择深色主题。

根据操作系统是设置为深色还是亮色模式,整个页面在深色上显示为浅色,反之亦然,基于用户代理样式表。开发者没有额外提供 CSS 来改变段落文本或页面的背景颜色。

请注意,<fieldset> 元素的背景颜色是如何根据是否启用了深色模式而改变的,它遵循了开发者在页面上提供的内联样式表的规则。它要么是gainsboro,要么是darkslategray

上图是亮色模式(light)下,由开发者和用户代理指定的样式。根据用户代理的样式表,文本是黑色的,背景是白色的。<fieldset>元素的背景颜色是gainsboro,由开发者在内联的式表中指定的颜色。

上图是暗色模式(dark)下,由开发者和用户代理指定的样式。根据用户代理的样式表,文本是白色的,背景是黑色的。<fieldset>元素的背景色是darkslategray,由开发者在内联样式表中指定的颜色。

按钮<button>元素的外观是由用户代理样式表控制的。它的颜色被设置为ButtonText系统颜色,其背景颜色和边框颜色被设置为ButtonFace系统颜色。

现在注意<button>元素的边框颜色是如何变化的。border-top-colorborder-bottom-color的计算值从rgba(0,0,0,.847)(偏黑)切换到rgba(255, 255, 255, .847)(偏白),因为用户代理根据颜色方案动态地更新ButtonFace。同样适用于<button>元素的color属性,它被设置为相应的系统颜色ButtonText

看上去不错,但这也引出另一个新的概念,系统颜色

CSS 系统颜色

上面的示例,虽然在<button>元素上没有设置CSS,但其背景颜色和边框颜色等会根据用户代理来自动调整,其中原委是调用了系统颜色,比如ButtonFaceButtonText等。那么什么是系统颜色呢?它又能帮我们做什么?

为了解释 CSS 系统颜色,我们从实现网站暗黑模式主题色着手。对于大多数人来,给自己Web应用或页面实现一暗黑模式主题,首先会想到使用 CSS 自定义属性和prefers-color-scheme媒体查询来实现:

:root {
    --color-bg: #fff;
}

@media (perfers-color-scheme: dark) {
    :root {
        --color-bg: #000;
    }
}

html {
    background-color: var(--color-bg);
}

事实上,上面这种方案并不是最佳的方案。更好的方案是不需要使用 CSS 来控制页面的背景颜色。应该 让浏览器(客户端)来决定文档的背景颜色。也就是前面提到的,使用color-sccheme: light dark来实现。

重温一下 color-scheme

color-scheme改变页面的默认文本和背景颜色,以匹配当前系统外观(主题色)。

上面一节介绍color-scheme的时候,向大家展示了标准的表单控件会随着 color-scheme 根据系统颜色自动改变其外观。事实上,除了标准的表单控件之外,还有滚动条和其他命名的系统颜色也会根据 color-scheme 自动跟着用户的客户端改变其外观。

也就是说,当你在 CSS 中未显式地为每种模式下文档设置背景颜色时,浏览器(客户端)会替你设置一个。这也意味着我们需要在浅色背景上获得深色文本(亮色模式),在深色背景上获得浅色文本(暗色模式)。即:

:root {
    color-scheme: light dark
}

注意,这样做的最大优势就是 不需要挑选或定义任何颜色(为不同模式)。用户代理(你的浏览器)样式表会帮助我们处理这些细节。因此,用户得到的调色板更接近于他们在操作系统中使用其他本地应用程序的体验。

例如,在没有任何硬编码颜色值的情况下,Safari浏览器使用了与macOS系统上的信息应用相同的“系统(system)”黑色阴影。如果你的系统是Window,那就会采用Window系统的信息。

这样做除了让用户获得一种感觉更接近于终端用户在其环境中的体验的好处之外,开发者不必选择一个黑色和白色来设置为变量,然后在多个地方使用变量

但这样做并不代表是完全没有问题。比如 Jim Nielsen他的博客中分享的一个案例(以他自己的博客为例)。即在博客中创建了一个下拉菜单效果,并且没有给该元素明确的设置背景颜色,这个时候系统将下拉菜单的背景颜色默认为 transparent。当你展开下拉菜单时,效果无法让你忍受:

如果要这样的问题,传统上(或者说大部分开发者)首先会在下拉菜单上设置一个与<html><body>元素相同的背景颜色。但这是有一个前提的,<html><body>有显式设置一个背景颜色值,如果没有这个前提怎么办呢?

简单地说,解决方案就是使用 CSS 系统颜色:

CSS有标准化的语义系统颜色。它们是在 CSS颜色模块 Level 4(CSS Color Module Level)规范中规则的。例如,Canvas(不是HTML中的<canvas>标签)用于应用程序内容或文档的背景,而 CanvasText 则用于应用程序内容或文档中的文本。

只需要在 CSS 中添加:

:root {
    color-scheme: light dark;
}

.dropdown {
    background-color: Canvas;
}

CSS颜色模块 Level 4(CSS Color Module Level)规范 中列出所有 CSS 系统颜色

但这些颜色并不是在所有的浏览器中都是完全相同的(或者至少是试图相同的),比如在Safari浏览器暗色模式下时Canvas的颜色值是#1e1e1e,在Chrome浏览器暗色模式下时,Canvas的颜色值是#121212

但是它们被允许由“用户、浏览器或操作系统的选择”来设置。也就是说,如果你倾向于浏览器为你做出的选择(浏览器认为是最好的颜色),那么你就可以使用 CSS 系统颜色。

比如下面这个 @Chris Coyier 在 Codepen 上向大家展示的示例

html {
    color-scheme: light dark;
}

body {
    background: Canvas;
    font: 100%/1.4 system-ui;
}

.card {
    border: 5px solid LinkText;
}

h3 {
    background: ButtonFace;
    color: ButtonText;
    border-bottom: 10px solid VisitedText;
}

p {
    font-family: ui-monospace, system-ui;
}

.button {
    background: ActiveText;
    color: Field;
}

footer {
    background: Highlight;
}

切换你系统设置,可以看到下面这样的效果:

在这个示例中,作者除了使用了系统颜色(比如CanvasLinkTextButtonFaceVisitedTextActiveTextFieldHighlight)还使用了一种系统字体,即 system-ui。 同样的,在不同的浏览器和操作系统中,它不会是相同的字体。有些开发者会将这些不同的字体都加进font-family系列中:

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}

/* 或 */
body {
    font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
}

但更多的时候是使用 PostCSS插件(postcss-font-family-system-ui来做这样的事情:

/* Input CSS */
body {
    font: 100%/1.5 system-ui;
}

/* Output CSS */

body {
    font: 100%/1.5 system-ui, -apple-system, Segoe UI, Roboto, Noto Sans, Ubuntu,
    Cantarell, Helvetica Neue;
}

除此之外,PostCSS的 Autoprefixer 也有这方面的能力

有关于系统字体更多的介绍可以阅读 Marcin Wichary 的 《Using UI System Fonts In Web Design: A Quick Practical Guide》和 Jim Nielsen 的《Leveraging System Fonts on the Web》。

我们曾经需要用一大堆命名的字体这个做法不需要再使用了,现在 CSS 帮助我们实现了。

似乎有点跑题了,我们还是回到系统颜色这个话题中来。

或许你会担心系统颜色的支持度(浏览器对其支持)。就我个人观点而言,作为开发者我们不应该拒绝一切更好的特性,特别是能给用户带来更好体验的特性。我们应该通过别的方式,让这一切变得更好。比如,我们只需要通过添加额外的几行 CSS 代码,就可以让这一切变得更美好:

/* 第一步:声明暗黑模式(Dark Mode)下颜色 */
:root {
    --c-bg: #fff;
    --c-text: #000;
}

@media (prefers-color-scheme: dark) {
    :root {
        --c-bg: #000;
        --c-text: #fff;
    }
}

/* 第二步:对于不支持 color-scheme 的浏览器,做相应的处理,采用定义的自定义属性 */
@supports not (color-scheme: light dark) {
    html {
        background: var(--c-bg);
        color: var(--c-text);
    }
}

/* 第三步:为支持自动 dark/light 模式的浏览器以及系统颜色,添加相应的 CSS */
@supports (color-scheme: light dark) and (background-color: Canvas) and (color: CanvasText) {
    :root {
        --c-bg: Canvas;
        --c-text: CanvasText;
    }
}

/* 第四步:仅用于iOS上的Safari 浏览器 */
@supports (background-color: -apple-system-control-background) and (color: text) {
    :root {
        --c-bg: -apple-system-control-background;
        --c-text: text;
    }
}

是不是对上面示例中的-app-system-control-background感到好奇。莫急,请继续往下阅读。

CSS 中能实现 theme-color?

我们前面看到的theme-color<meta>name。你可能会问,在CSS中能实现类似theme-color的效果?或者会问,如果这些样式的控制希望在 CSS 中,而不是在 HTML的 <meta>源数据中。类似这样,我们在 CSS 使用下面这样的代码能覆盖<meta>theme-color的相关信息:

<!-- HTML 中的声明 -->
<meta name="theme-color" content="#ecd96f" >

/* CSS 中声明来覆盖 HTML的声明 */
meta[name="theme-color"] {
    attr-content: "#ecd96f"
}

@media (prefers-color-scheme: dark) {
    meta[name="theme-color"] {
        attr-content: "#f38daef"
    }
}

注意,上面的代码仅仅是一种 YY 的方式。目前在规范或相关的草案中并没有这样的提议!

虽然上面的代码是 YY 的一段代码,但在CSS中很多属性,都是先这样YY得来的。或者说,如果有一天在:root元素上能使用一个叫theme-color的属性,那又会是怎么样呢?

:root {
    theme-color: #ecd96f;
}

@media (prefers-color-scheme: dark) {
    :root {
        theme-color: #f38daef;
    }
}

看上去是不是有点像color-scheme。我们甚至可以这样想:

:root {
    theme-color: #ecd96f #f38daef; /* 前者是亮色模式下的theme-color值,后者是暗色模式下theme-color值 */
}

这里的问题是,theme-color并不像其他标准的CSS属性那样工作,因为它只能用于:root选择器。例如,background-color属性可以应用于任何选器,从:rootdiv.classname。但是,在上面的例子中,一个主题颜色(theme-color)只适用于:root元素,这样一来,theme-color能否是一个CSS属性又变得没有那么重要。

再换过一个角度来聊theme-color。时至今日,CSS自定义属性 越来越受到广大开发者的青眯,如果theme-color是CSS中的一个自定义属性,那又将会是什么样?这可能有很大的意义,特别是考虑到 Safari 已经为你选择了一个默认值。所以在实际上使用的时候,只需要考虑覆盖它的默认值即可。

这个想法事实上和上面介绍的 CSS 系统颜色有点类似。如果有一些系统变量,浏览器会在他们自己的 UA 样式表中设置为默认值,那么开发者,就可以在 CSS 中轻易覆盖它们。想象一下,如果这些系统变量都是以--system-为前缀,比如:

:root {
    --system-theme-color: #ecd96f;
}

@media (prefers-color-scheme: dark) {
    :root {
        --system-theme-color: #f38daef;
    }
}

其实,在前面介绍theme-color的时候,我们有一个是使用 JavaScript 来改变 <meta> 标签theme-color的示例。如果说,CSS 真的能像这样通过 自定义属性来改变的话,那么使用 JavaScript 和 CSS 会让事情变得更为简单。比如:

/* CSS */
:root {
    --system-theme-color: #ecd96f;
}

// JavaScript
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
    document.documentElement.style.setProperty('--system-color', '#f38daef')
}

或者:

window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
    if (event.matches) {
        //dark mode
        document.documentElement.style.setProperty('--system-color', '#f38daef')
    } else {
        //light mode
        document.documentElement.style.setProperty('--system-color', '#ecd96f')
    }
})

搜噶!这一切仅是想法。你也可以发动你的大脑,说不定会有更好的方案。

theme-color 的使用案例

现在在社区中有一些网站能看到theme-color的使用案例。注意,这里指的是<meta>上的theme-color

先来看 Max Böck 的个人博客,你改变主题色的时候,会看到theme-color也做出相应变化:

实现这个效果思路很简单,作者把 <meta>theme-color和 CSS自定义属性结合起来一起使用了。首先在<meta>标签上设置theme-color的值:

<meta name="theme-color" content="#f3e1d8">

同时在:root定义页面元素背景和文本所需要的定义属性:

:root{
    --color-bg: #ffffff;
    --color-bg-offset: #f7f7f9;
    --color-text: #373a3c;
    --color-text-offset: #818a91;
    --color-border: #eceeef;
    --color-primary: #ff335f;
    --color-primary-offset: #ff1447;
    --color-secondary: #43a9a3;
}

然后借助 JavaScript 脚本来改变这些值,比如:

// 仅展示用
button.addEventListener('click', e => {
    document.querySelector('meta[name="theme-color"]').setAttribute("content", '#252526')
    document.documentElement.style.setProperty('--color-bg', '#252526')
})

再来看 Dave 的博客,他在他的博客模板中只使用了一行代码:

<meta name="theme-color" content="{% if page.colour %}{{ page.colour }}{% else %}#f2eac9{% endif %}">

上面代码是什么模板制作的,我也没整明白。整个博客源码可以在 Github 上获取

博客文章中对链接和图标使用不同的颜色,现在也可以在标签中使用:

Dave 也在他自己 Twitter 上分享了这份喜悦

Manuel Matuzovic 在 Codepen 上分享了一个表单验证的案例:

表单在验证时改变theme-color的值。比如提交无效数据时,标签栏会变成红色,提交有效数据时,标签栏就会变成绿色。实现这个效果的代码很简单:

<!-- HTML -->
<head>
    <meta name="theme-color" content="#123456">
</head>
<body>
    <form action="#">
        <label for="email">E-Mail</label>
        <input type="email" name="email" id="email" required>
        <button>Submit</button>
    </form>

    <div aria-live="assertive"></div>
</body>    

// JavaScript
const email = document.querySelector('input')
const themeColor = document.querySelector('meta[name="theme-color"]')
const msg = document.querySelector('[aria-live]')
let color = '#FA0000'
let message = 'Error message'

document.querySelector('button').addEventListener('click', (e) => {
    e.preventDefault()

    email.reportValidity()
    email.setAttribute('aria-invalid', true)

    if (email.validity.valid) {
        color = '#00FF00'
        message = "Success message!"
        email.setAttribute('aria-invalid', false)
    }

    msg.textContent = message
    themeColor.setAttribute('content', color)
});

效果如下:

这个示例是不是为你打开了新的一扇门。以后在做一些验证类的交互操作时,除了以前的交互方式,还可以像上面示例一样,改变客户端的一些主题色。让用户有更直观的体感!

比如和Progress Bar 结合起来,完成不同的步数就改变theme-color的值:

点击示例中的“next”和“pre”按钮,就能看到标签栏颜色随之改变:

同样的思路还可以运用于 Range Slider 这样的组件中,比如:

对于inputtyperange,它有minmaxvalue几个值,使用 CSS 的伪类选择器:in-range:out-of-range可以改变 input 的 UI:

input[type=number]:in-range {
    outline: lightgreen solid 1px;
}

input[type=number]:out-of-range {
    outline: red solid 1px;
}

我们利用其范围值来改变theme-color值:

input的值不是在min~max范围就会变成红色:

Stuart Clarke-Frisby 在 Chris Coyier 的Twitter 一下,建议 Chris Coyier 根据滚动事件来改变标签栏的主题色:

而且 Max 使用 Intersection Observer 根据视窗中当前部分背景颜色来改变theme-color的值

来看关键的 JavaScript 代码:

const setThemeColor = (color) => {
    const meta = document.querySelector('meta[name="theme-color"]')
    if (meta) {
        meta.setAttribute('content', color)
    }
}

if ("IntersectionObserver" in window) {
    const observer = new IntersectionObserver(entries => {
        entries.forEach(entry => {
            const { isIntersecting, target } = entry
            if (isIntersecting) {
                const color = window.getComputedStyle(target).getPropertyValue("background-color");
                setThemeColor(color)
            }
        })
    }, {
        root: document.getElementById('viewport'),
        rootMargin: "1px 0px -100% 0px",
        treshold: 0.1
    })
    
    document.querySelectorAll('.section').forEach(section => {
        observer.observe(section)
    })
}

你将看到的效果如下所示:

最后再看一个示例,根据图片提取出颜色,然后作为theme-color的值:

<script type="module">
    import fastAverageColor from "https://cdn.skypack.dev/fast-average-color@6.4.0";
    const fac = new fastAverageColor();
        
    fac.getColorAsync(document.querySelector('img'))
        .then(color => {
            document.querySelector('meta[name="theme-color"]').setAttribute('content', color.rgba)
        })
        .catch(e => {
            console.log(e);
        });
</script> 

<img src="/amy-humphries-2M_sDJ_agvs-unsplash.jpg" alt="A sea star on blue sand." />

效果如下:

这几个案例,仅仅是向大家展示 theme-color 的使用效果。但我更喜欢的是这样的一个方向,说不定哪一天就可以满足产品经理的需求,“让你的APP能根据用户手机壳调整主题色”。当然,我想你肯定会想出更多更有创造性的效果,或者换着各种不同姿势使用theme-color,给自己的应用带来意想不到的效果。

小结

我们以前更多的了解的是使用 CSS 媒体查询相关的特性,为用户在终端上的偏好设置提供不同的 UI界面或者效果。比如我们熟悉的暗黑模式,就是采用了这方面特性。那么随着 <meta>theme-color的到来,我们有了更多的选择,也能为用户提供更好和体验,比如文章中示意,在表单验证的时候,根据不同的验证结果来改变状态栏的效果。除了theme-color之外,还有 CSS的系统颜色,也能让我们自己的Web应用能具备更好的体验。

我在想,随着技术不断的革新,以及设备不断的智能化,在未来的某一天,我们能更智能的为用户提供更好的体验。