CSS 变量使用详解

配图源自 Freepik

这篇文章你将学到以下内容:

  • CSS 变量
  • CSS 常用函数
  • iPhone X 系列机型适配
  • CSS At-rules 和媒体查询
  • 深色模式适配

一、简述

CSS 变量(CSS Variables),也称作 CSS 自定义属性(CSS Custom Properties),它是带有前缀 -- 属性名,且带有值的自定义属性。然后通过 var 函数在全文范围复用。

至于为什么采用 --,大概是因为 @Less 占用了,$Sass 占用了吧。

1.1 语法

定义 CSS 变量的语法非常简单,在变量名称之前添加两个短横线 --

--<custom-property-name>: <declaration-value>

其中 <custom-property-name> 表示变量名称,<declaration-value> 表示变量值,形如:--*。这类自定义 CSS 属性与 colorfont-sizebackground-image 等属性并没有什么不同,只是它没有默认含义罢了,它必须通过 var() 函数复用之后,才会产生意义。

其中「变量名称」命名约束是比较宽松的,可以是数字、字母、下划线 _、短横线 - 的组合,但不能包含 $[^(% 等字符。比如:

--some-keyword: left;
--some-color: #f00;
--some-complex-value: 3px 6px rgb(20, 32, 54);

甚至可以是以数字开头、也可以是中文、韩文等。

:root {
 --红色: #f00; /* 有效 */
 --1: 1px; /* 有效 */
}

body {
 background-color: var(--红色);
 height: var(--1);
}

当然,实际项目中,千万别以这种花里胡哨、奇奇怪怪的组合来命名变量名称,主要是避免被打。建议使用 kebab-case 方式进行命名,比如 --theme-primary 等。

请注意,CSS 变量名称是大小写敏感的,--foo--Foo 是两个不同的变量。这一点与 CSS 属性大小写不敏感是有区别的。

1.2 作用域

同一个 CSS 变量,可以在多个选择器内声明,读取顺序与 CSS 匹配规则一致,优先级最高的生效。请注意,CSS 变量并没有 !important 用法,变量的覆盖规则由 CSS 选择器权重决定。

一般情况下,全局性变量放在 :root 内声明,也可以在任意元素中声明 CSS 变量,视实际情况而定即可。如果是小程序,则在全局样式 app.wxsspage 内声明。

:root 这个 CSS 伪类匹配文档树的根元素。对于 HTML 来说,:root 表示 <html> 元素,除了优先级更高之外,与 html 选择器相同。

:root {
  --theme-primary: #f00; /* 全局可复用 */
}

header {
  --theme-primary: #0f0; /* 仅 header 范围内可复用 */
}

section {
  --theme-primary: #00f; /* 仅 section 范围内可复用 */
}

比如 <section> 内使用 color: var(--theme-primary),生效的将会是 color: #00f。再者,以下示例中,在 <div id="error"></div> 中引用 --color 变量,最终生效的是 ID 选择器的变量值。

:root {
  --color: #f00;
}

div {
  --color: #0f0;
}

#id {
  --color: #00f;
}

总的来讲,CSS 变量是有作用域概念的,它只能作用于自身或后代元素,而兄弟元素、祖先元素都是不用享用的。

可以试下这个示例:css-variable-scope-demo

1.3 兼容性

兼容性如下,还是挺不错的(如果忽略 IE 的话),更多请看 Can I use

很棒 🙄,IE 全系不支持,骂骂咧咧地说:还是用 SCSS 或 LESS 吧。可以参考下这个项目:css-vars-ponyfill

对于不支持 CSS 变量的浏览器,可以采用如下方式兼容处理:

:root {
  --color-primary: #00f;
}

a {
  color: #00f;
  color: var(--color-primary);
}

也可以使用 @supports 规则,然而它也不兼容 IE 浏览器。

@supports (--foo: 0) {
  /* supported */
}

@supports (not (--foo: 0)) {
  /* unsupported */
}

二、JavaScript 操作

利用 CSS.supports() 方法即可判断当前浏览器是否支持 CSS 变量,如下:

const isSupported = window.CSS.supports('--foo', 0)

由于 CSS 变量就是自定义的 CSS 属性嘛,因此按照平常设置 CSS 属性的方式去操作即可,如下:

const element = document.querySelector('selectors')

// 定义 CSS 变量
element.style.setProperty('--color', '#f00')

// 读取 CSS 变量
element.style.getPropertyValue('--color', '#f00')

// 删除 CSS 变量
element.style.removeProperty('--color')

另外有一个比较奇怪的用法(来自 EXAMPLE 7),如下:

:root {
  --foo: if(x > 5) this.width = 10;
}

尽管这个属性值是「无用」的,不会使得任意 CSS 属性产生实际效果,但是这个 CSS 变量定义是「有效」的。它可以被 JavaScript 读取,至于有什么用,我也不知道。

二、CSS 函数

2.1 var 函数

var() 函数用于读取 CSS 变量,它可以替代元素中「任何属性」中的「值的任何部分」,不能作为属性名、选择器或其他处理属性值之外的值。

语法如下:

var(<custom-property-name>[, <declaration-value>])
  • <custom-property-name> 表示自定义属性名
  • <declaration-value>(可选)表示声明值(后备值),仅自定义属性没有定义时,它才会有效(类似 ES6 中的函数参数设定默认值)。

2.1.1 CSS 变量不合法的缺省特性

看看以下示例,变量 --color 的值为 20px,显然它作为 background-color 值的话是无效的,那么 <body> 会显示什么背景颜色呢?红色?绿色?还是...

body {
  --color: 20px;
  background-color: #f00;
  background-color: var(--color, #0f0); /* 正确语法,与 background-color: 20px 有着本质上的区别 */
}

它最终生效的属性值为 transparent,即 background-color 的默认值,因此相当于:

body {
  --color: 20px;
  background-color: #f00;
  background-color: transparent;
}

👆 可看 EXAMPLE 13

但请注意,以下示例生效的是 background-color: #f00,就怕有人看到上面示例之后,对原来的认知产生怀疑,特意说明下。

body {
  background-color: #f00; /* 有效 */
  background-color: 20px; /* 语法错误,这条规则声明被丢弃,因此上一条规则生效 */
}

因此,当 CSS 变量值不合法时,生效的是 CSS 属性的“默认值”。

但注意,CSS 变量值不合法并不能使得 <declaration-value> 声明值生效,它仅限于 CSS 变量没有定义才会生效(类似函数参数的默认值仅实参为 undefined 才会生效,即便是 null 等 falsy 实参也不会使其生效一样)。

为什么这里默认值要打双引号呢,原因是标准 EXAMPLE 13 部分明确说明了:

If the property was one that’s inherited by default, such as color, it would compute to the inherited value rather than the initial value.

也就是说,如果一个 CSS 属性是可继承的,那个当它应用了一个不合法的 CSS 变量值,最终生效的是其继承值,而不是默认值。比如:

<style>
  :root {
    --not-a-color: 20px;
  }

  body {
    color: #f00;
  }

  p {
    color: var(--not-a-color);
  }
</style>
<body>
  <p>字体会是什么颜色呢?</p>
</body>

你看最终 <p> 生效的 color 是其从 <body> 中继承过来的 #f00 红色,而不是 color 的默认颜色 canvastext

插个话题,我很好奇 canvastext 颜色是什么颜色,一般来说它会是黑色 rgb(0, 0, 0),然后我尝试将系统调至深色模式,然而它并不会默认变为白色,哈哈。然后我翻查了下标准,发现它跟 <system-color> 有关,它一般由浏览器来定义(如下),可看 6.2 System Color 章节。

因此,比较严谨的说法是:当 CSS 变量值不合法时,生效的是 CSS 属性的继承值或初始值。

2.1.2 var 函数的尾随空格

<style>
  body {
    --size: 20;
    font-size: var(--size)px;
  }
</style>
<body>
  <p>字号是多大呢?</p>
</body>

猜一下 font-size 会是预期的 20px 吗?它不是,如下图:

请注意,浏览器最终解析出来的规则是:font-size: var(--size) px;,它在 var(--size)px 之间多了一个「空格」,因此这条规则是无效的(注意并不是引用 CSS 变量无效),所以字号是浏览器默认字体大小 16px

如果你使用诸如 VS Code 等编辑器,它一般会有 semi-colon expected 错误提醒的,如果保存自动格式化,它将会被保存为:font-size: var(--size) px;

这种情况可结合 calc() 函数处理,比如:

body {
  --size: 20;
  font-size: calc(var(--size) * 1px); /* 这样就能正常计算得出 20px 了 */
}

但个人更推荐这样用:对于一些长度、大小等 CSS 属性值,在定义 CSS 变量时,应带上单位:

body {
  --size: 20px;
  font-size: var(--size);
}

请注意,如果变量值包含单位,就不能写成字符串形式。

body {
  --size: '20px';
  font-size: var(--size); /* 注意,CSS 变量引用的语法是有效的,但经 CSS 解析器计算之后,其值并不符合 font-size 属性值的要求,因此被判定为语法错误,规则会被丢弃。 */
}

相当于 font-size: '20px'; 语法错误,规则会被丢弃,因此取其继承值或默认值。

2.1.3 CSS 变量的相互传递性

我们在某个选择器中定义了一个 CSS 变量,它除了在子元素中被复用,它本身作用域内也可以复用,而且与编写顺序无关。比如:

body {
  --size: 20px;
  font-size: var(--size);
}
body {
  font-size: var(--size);
  --size: 20px;
}

以上两个示例,均是有效的。后者并不会因为 --size: 20px; 定义在后,就不会生效。这样规则,对于我们通过 JavaScript 动态设置 CSS 变量有着非常重要的意义。

2.2 calc 函数

calc() 语法非常地简单,如下:

property: calc(expression)

该函数接收一个表达式作为它的参数,表达式的返回值作为 calc() 函数的值。表达式可以是 +-*/ 的组合,而且可以混用不同单位进行运算。

它同样支持 CSS 变量,例如:

.foo {
  --height: 30px;
  width: calc(100% - 30px);
  height: calc(100vh - var(--height))
}

注意点:

  • 对于 +- 运算,运算符两边必须要有「空格」,而 */ 运算则没有要求,因此建议都加上空白符。
  • 对于 * 运算,参与运算的至少有一个数值(<number>),且不能为 0
  • 对于 / 运算,运算符 / 右侧必须是一个数值(<number>)。
  • calc() 函数支持嵌套写法,但其实被嵌套的 calc() 函数只会被当做普通的括号,因此函数内直接使用括号就好了。

那么嵌套语法有什么用呢,比如:

.foo {
  --widthA: 100px;
  --widthB: calc(var(--widthA) / 2);
  --widthC: calc(var(--widthB) / 2);
  width: var(--widthC);
}

那么,以上 --widthC 的值就会变成 calc(calc(100px / 2) / 2),即 25px

Web 前端总是绕不开兼容性,那么看下 calc() 函数的兼容性如何:

绿悠悠的一片,甚好!可以看到 IE9 以上都支持,可 IE 浏览器不支持嵌套写法,由于 IE 浏览器都不支持 CSS 变量,因此这个无伤大雅。

2.3 env 和 constant 函数

2017 年 Apple 公司发布了 iPhone X 和 iOS 11,开启了「刘海屏」和底部小横条之路。

于是就有了「安全区 Safe Area」之说(详见):

A safe area defines the area within a view that isn’t covered by a navigation bar, tab bar, toolbar, or other views a view controller might provide.

简单来讲,以 iPhone X 为例,其安全区是指不受刘海(Sensor Housing)、底部小横条(Home Indicator)、设备圆角(Corners)影响的区域,如下图的浅蓝色区域所示,其中粉色部分是指浏览器默认的 Margin 值,通常为了抹平各浏览器不同的外边距,都会设置 * { margin: 0 }

我们知道,Viewport 是规则的矩形,如果显示设备的屏幕是不规则(比如圆形)的话,页面中的某些部分就会被裁剪。那么 viewport-fit 可以通过设置可视 Viewport 大小来控制裁剪区域。

其中 viewport-fit 提供了 auto(默认)、containcover 三种属性值(详见):

<meta name="viewport" content="viewport-fit=auto|contain|cover" />
属性值 描述
auto 默认值,表现与 contain 一致,Viewport 会显示在「安全区」之内,相当于 viewport-fit: contain
contain 将可视 Viewport 设置为页面所有内容均可见的最大矩形。
cover 将可视 Viewport 大小设置为显示设备屏幕的外接矩形。

以圆形屏幕为例:

Apple 公司为了适配旗下的全面屏设备,(iOS 11 起)WebKit 内核的浏览器中定义了 safe-area-inset-* 四个环境变量。

  • safe-area-inset-top
  • safe-area-inset-right
  • safe-area-inset-bottom
  • safe-area-inset-left

需要注意的是,在竖屏和横屏状态下,safe-area-inset-* 值是不同的。比如,竖屏状态下环境变量 safe-area-inset-leftsafe-area-inset-right 的值为 0,横屏状态下环境变量 safe-area-inset-top 的值为 0

上图源自 Deng's Blog

通过 env()constant() 函数就能引用以上几个环境变量,对于不支持 env()constant() 的浏览器,包含它的样式规则将被忽略。

自 Safari Technology Preview 41 和 iOS 11.2 beta 起,constant() 函数已被移除,并用 env() 函数替换(详见)。可为了兼容性,一般两个都会写。

另外,若要环境变量 safe-area-inset-* 生效,需将页面设置为 viewport-fit: cover

接下来,会介绍如何适配 iPhone X 系列刘海屏手机,有以下示例:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
    <title>Document</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }

      body {
        width: 100%;
        background-color: darkorange;
      }

      .container {
        height: 100%;
        min-height: 200vh;
        background-color: forestgreen;
      }
    </style>
  </head>
  <body>
    <div class="container"></div>
  </body>
</html>

当我们不做任何处理,以上示例在 iPhone X 系列手机横屏状态下,左右边框会空出一部分,究其原因就是 Safari 浏览器会将网页内容置于「安全区之内」,相当于 viewport-fit: contain

当我们将 <meta> 标签内的 viewport-fit 改为 cover 之后,并在页面中添加一首诗。

这样,Viewport 就占满了显示设备最大的矩形,但因为设备的刘海、圆角等因素,会导致页面中的部分内容无法完全显示。

然后,我们试着在 container 内添加左右外边距,其值分别取 safe-area-inset-leftsafe-area-inset-right 环境变量。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover" />
    <title>Document</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }

      body {
        width: 100%;
        background-color: darkorange;
      }

      .container {
        height: 100%;
        min-height: 200vh;
        background-color: forestgreen;

        /* 新增 */
        margin-left: env(safe-area-inset-left);
        margin-right: env(safe-area-inset-left);
      }
    </style>
  </head>
  <body>
    <div class="container">
      <article>
        <h1>蒹葭</h1>
        <p>蒹葭苍苍,白露为霜。所谓伊人,在水一方。溯洄从之,道阻且长。溯游从之,宛在水中央。</p>
        <p>蒹葭萋萋,白露未晞。所谓伊人,在水之湄。溯洄从之,道阻且跻。溯游从之,宛在水中坻。</p>
        <p>蒹葭采采,白露未已。所谓伊人,在水之涘。溯洄从之,道阻且右。溯游从之,宛在水中沚。</p>
        <p></p>
      </article>
    </div>
  </body>
</html>

横屏、竖屏显示如下:

考虑 env()constant() 函数兼容性的写法如下:

@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
  .container {
    /* 请注意,constant 和 env 先后顺序如下 */
    margin-left: constant(safe-area-inset-left); /* 兼容 iOS < 11.2 */
    margin-left: env(safe-area-inset-left); /* 兼容 iOS >= 11.2 */

    margin-right: constant(safe-area-inset-right);
    margin-right: env(safe-area-inset-right);
  }
}

2.4 max 和 min 函数

从文章排版来看,这是极不美观的,我们希望在左右两边再加点内边距。PS:上述图片为了更方便对比,采用了外边距(Margin),接下来会将其修改为内边距(Padding)。

.container {
  /* 这里将原先的 margin 修改为 padding */
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-left);
}

但有个小小的需求,我们只希望竖屏状态下,添加 10px 的左右内边距,而横屏状态下取 safe-area-inset-* 的值就好。

它们提供了两个 CSS 函数:min()max(),利用它们就能实现这需求。

.container {
  /* 这里使用到 max() 函数,表示取二者最大值。 */
  padding-left: max(10px, env(safe-area-inset-left));
  padding-right: max(10px, env(safe-area-inset-left));
}

效果如下:

如果考虑兼容性的话,可以使用 @supports 语法来处理:

@supports (padding: max(0px)) {
  .container {
    padding-left: max(10px, env(safe-area-inset-left));
    padding-right: max(10px, env(safe-area-inset-left));
  }
}

对于 max()min() 的语法和使用非常地简单,分别表示取最大值和最小值。

两者语法是一致的,以 max 为例:

property: max(expression [, expression])

它接受一个或多个值,若有多个值则采用逗号 , 分隔,选择最大的值作为 CSS 属性的值。每个值除了可以是直接数值,还可以是数学运算(如 calc())、其他表达式(如 attr())。

还支持嵌套 max()min() 函数,需要时可以使用小括号 () 来设定运算顺序。

兼容性如下,一如既往 IE 全系不支持:

至此,相信你对 iPhone X 等机型的适配有更深刻的了解,适配起来就完全没有压力了。

三、CSS At-rules

一个 at-rule 是一个 CSS 语句,它以 @ 符号开头,后接一个标识符,并包括直到下一个分号 ; 的所有内容或下一个 CSS 块,以先到者为准。

主要可分为不可嵌套、可嵌套两类:

不可嵌套 at-rule:

  • @charset:指定样式表中使用的字符编码。
  • @import:导入其他外部样式表。
  • @namespace:指示 CSS 引擎必须考虑XML命名空间。

可嵌套 at-rule:

  • @media:用于基于一个或多个媒体查询的结果来应用样式表中的一部分。
  • @font-face:指定一个用于显示文本的自定义字体。
  • @keyframs:通过在动画序列中定义关键帧的样式来控制 CSS 动画序列中的中间步骤。
  • @supports:指定依赖于浏览器中的一个或多个特定的 CSS 功能的支持声明。
  • @document:根据文档的 URL 限制其中包含的样式规则的作用范围(实验特性)。
  • @page:用于在打印文档时修改某些 CSS 属性。

每个 at-rule 规则都有不同的语法,有一部分 at-rule 可以归为一类:条件规则组

这些规则组所指的条件总等效于 truefalse,如果为 true 那么它里面的 CSS 语句生效。

本文仅介绍 @supports@media,其他规则请看 CSS At-rules

3.1 @supports

@supports 常用于 CSS 兼容性判断。

它由一组支持条件和一组样式声明组成。支持条件可以是一个或多个条件使用逻辑与 and、逻辑或 or、逻辑非 not 组合而成。

  • 单一条件:由一个 CSS 属性和属性值组成,中间用分号 ; 隔开。
@supports (transform-origin: 5% 5%) {
  /* 样式声明 */
}

transform-origin 的实现语法认为 5% 5% 是有效的值,表达式会返回 true,此时规则内声明的样式就会生效。

  • 多个条件:使用 notandor 操作符组合。

相当于 JavaScript 中的 !&&|| 操作符啦,需设定运算顺序,则使用括号包裹。

/* 当 transform-origin: 10em 10em 10em 无效时,表达式返回 true */
@supports not (transform-origin: 10em 10em 10em) {
  /* 样式声明 */
}
/* 当所有条件同时为真时,表达式才返回 true */
@supports (display: table-cell) and (display: list-item) {
  /* 样式声明 */
}
/* 当条件至少有一个为真时,表达式才返回 true */
@supports (transform-style: preserve) or (-moz-transform-style: preserve) {
  /* 样式声明 */
}

还有一个实验性的语法:selector(),有兴趣请看这里

兼容性仍然是 IE 全系不支持,呵呵~

3.2 @media 介绍

媒体查询(Media Queries),在网页开发中是非常常用的。浏览器给 Web 提供了一些媒体特性(Media Features),它描述了 User Agent、输出设备、浏览器环境的具体特征。网页开发者可根据这些特性,来提供更好的用户体验。

比如:

@import 'common.css' screen, projection;

@media screen and (min-width: 480px) {
    /* ... */
}
<link rel="stylesheet" src="styles.css" media="screen" />
<link rel="stylesheet" href="mobile.css" media="(max-width: 480px)" />
// 如果参数是 CSS 声明(也就是出现了冒号),外面需要有个括号,否则语法不正确。
if (window.matchMedia('(max-width: 480px)').matches) {
    // ...
}

使用媒体查询最常见的是 @media 方式,但是在 HTML 和 JavaScript 同样是可以使用的,后者用得较少。

媒体查询可以这样使用:

需要注意的是,当媒体查询作用于 <link> 元素时,即使媒体查询结果为 false,该样式表仍会被下载,只是样式内容不被作用而已。

3.2.1 媒体查询语法

每条媒体查询语句(不区分大小写),由一个可选的「媒体类型」和任意数量的「媒体特征」表达式构成。同时可使用多种「逻辑操作符」合并多条媒体查询语句。

当媒体查询语句中指定的媒体类型与设备匹配时,这条媒体查询结果返回 true。若是多条媒体查询语句组合,需每个条件均为 true,那么媒体查询结果才为 true

3.2.2 媒体类型(Media Types)

在媒体查询中,媒体类型(Media Types)是可选的,默认是 all 类型。一般是使用了 notonly 逻辑操作符,才需要显式指定。有以下这些:

媒体类型 描述
all 默认值,适用于所有设备
screen 适用于屏幕。
print 适用于在打印预览模式下的屏幕上查快递分页材料和文档。
speech 适用于语音合成器。

一部分媒体类型在 Media Queries Level 4 中已被弃用,可看这里

简单示例:

@media print {
  /* 针对打印设备 */
}

@media screen, print {
  /* 针对多种设备 */
}

3.2.3 媒体特性(Media Features)

媒体特性描述了 User Agent、输出设备,或是浏览环境的具体特征,更多请看 MDN

建议:使用前先弄清楚兼容情况

常见的媒体特性:

媒体特性 描述
aspect-ratio 视口(Viewport)的宽高比。
width 输出设备更新内容的渲染结果的频率。
height 视口(Viewport)的高度
hover 主要输入模式是否允许用户在元素上悬停。
orientation 视口(Viewport)的旋转方向。
prefers-color-scheme 探测用户倾向于选择亮色还是暗色的配色方案。
resolution 输出设备的像素密度(分辨率)。
scripting 探测脚本(例如 JavaScript)是否可用?

实际场景中用得较少的媒体特性:

媒体特性 描述
any-hover 是否有任何可用的输入机制允许用户(将鼠标等)悬停在元素上?
any-pointer 可用的输入机制中是否有任何指针设备,如果有,它的精度如何?
pointer 主要输入机制是一个指针设备吗?如果是,它的精度如何?
color 输出设备每个像素的比特值,常见的有 8、16、32 位。如果设备不支持输出彩色,则该值为 0。
color-gamut 用户代理和输出设备大致程度上支持的色域。
color-index 输出设备的颜色查询表(color lookup table)中的条目数量,如果设备不使用颜色查询表,则该值为 0。
display-mode 应用程序的显示模式,比如 Web App 中的 manifest 中的 display 成员所指定。
forced-color 检测 User Agent 是否限制调色板。
grid 输出设备使用网格屏幕还是点阵屏幕?
inverted-colors User Agent 或者底层操作系统是否反转了颜色。
light-height 环境光亮度。
monochrome 输出设备单色帧缓冲区中每个像素的位深度。如果设备并非黑白屏幕,则该值为 0。
overflow-block 输出设备如何处理沿块轴溢出视窗(viewport)的内容
overflow-inline 沿内联轴溢出视口(Viewport)的内容是否可以滚动?
prefers-contrast 探测用户是否有向系统要求提高或降低相近颜色之间的对比度。
prefers-reduced-motion 用户是否希望页面上出现更少的动态效果。
update 输出设备更新内容的渲染结果的频率。

在 Media Queries Level 4 已被弃用的媒体特性:

媒体特性 描述
device-aspect-ratio 输出设备的宽高比。
device-width 输出设备渲染表面(如屏幕)的宽度。
device-height 输出设备渲染表面(如屏幕)的高度。

对于 Media Queries Level 各级中新增或弃用的媒体特性,可看这里

对于表示「范围」的的媒体特性(比如 widthheight 等),可以使用前缀 min-*max-* 表示查询最小值、最大值。比如:

@media (width: 480px) {
  /* 当 Viewport 宽度等于 480px 时被应用 */
}

@media (min-width: 720px) {
  /* 当 Viewport 最小宽度大于等于 720px 时被应用 */
}

@media (max-width: 960px) {
  /* 当 Viewport 最大宽度不超过 960px 时被应用 */
}

以上这种写法,在 Media Queries Level 4 中支持以下范围语法,但目前兼容性非常的差,了解下就行了:

@media (min-width: 720px) and (max-width: 960px) {}

/* 等价于 */
@media ( 720px <= width <= 960px) {}

3.2.4 device-width 与 width 的区别

其中 device-width 表示设备的宽度,width 表示视口 Viewport 宽度。举个例子,在 PC 中打开了一个「非全屏」的浏览器窗口,那么 device-with 是显示器的宽度,是不可变的。而 width 则是浏览器内页面的 Viewport 宽度(注意,不能单纯地认为浏览器窗口的宽度),通过拖拽浏览器窗口等行为是可使其发生变化的。

但请注意,在移动端中,横屏与竖屏的切换,device-width 会相应跟着改变的。应对这种行为,可结合 orientation 这个媒体特性使用。

device-heightheight 同理。

3.2.5 逻辑操作符(Logical Operators)

通过逻辑操作符(andnotonly),可以创建复杂、多条件的媒体查询。其中 andnotonly 分别表示与、非、唯一的意思。

此外,还可以使用 ,(逗号)将多个媒体查询合并,只要任一查询结果为 true,整个查询语句就返回 true(跟逻辑或 or 行为类似)。

需要注意的是:

  • not 操作符不能用于否定单个查询条件,只能用于否定整个媒体查询。
  • 使用 notonly 操作符,必须指定「媒体类型」。
  • and 操作符可以将媒体类型、媒体特性或其他媒体功能组合在一起。
  • only 操作符,一般用于处理不支持「媒体特性」查询的语句旧版本浏览器。

下面看一些示例:

/* not 作用于整个媒体查询 */
@media not all and (monochrome) {}

/* 等价于 */
@media not (all and (monochrome)) {}

/* 而不是 */
@media (not all) and (monochrome) {}

再看:

@media not screen and (color), print and (color) {}

/* 等价于 */
@media (not (screen and (color))), print and (color) {}

再看:

/* 通过 and 可组合多个条件 */
@media screen and (min-width: 30em) and (orientation: landscape) {}

再看:

/*
  用户设备的最小高度为 680px 或为竖屏模式的屏幕设备,都会被应用此样式。
*/
@media (min-height: 680px), screen and (orientation: portrait) {}

再看这个示例,如果是不支持「媒体特性」查询的旧浏览器,那么以下样式将不会被应用,在这些浏览器看来,它将 only 识别为「媒体类型」了,但实际上并没有这样的媒体类型,因而将不会被应用。而在现代浏览器中,就没有任何影响。

@media only screen and (color) {}

/* 在旧版浏览器,相当于这样 */
@media only {}

/* 在现代浏览器,相当于这样 */
@media screen and (color) {}

3.3 @media 应用场景

  1. 浅色模式与深色模式

可以使用媒体特性 prefers-color-scheme 来判断是否在系统层面设置了深色模式。它有三个可选值:

  • light - 表示用户在系统层面使用了“浅色模式”。
  • dark - 表示用户在系统层面使用了“深色模式”。
  • no-preference - 理解为浏览器不支持设置主题色,或者浏览器支持设置主题色,但默认被设为“未设置”、“无偏好”等(也可简单理解为无默认值)。
/* 深⾊模式 */
@media (prefers-color-scheme: dark) {}

/* 浅⾊模式 */
@media (prefers-color-scheme: light) {}

通过 DOM API 提供的 matchMedia 接口,可以做一些判断:

if (window.matchMedia('(prefers-color-scheme)').media !== 'not all') {
  console.log('🎉 Dark mode is supported')
}

if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
  console.log('Dark mode is on.')
}

if (window.matchMedia('(prefers-color-scheme: light)').matches) {
  console.log('Light mode is on.')
}

想要监听浅色/深色模式的切换,可以这样做:

const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const lightModeMediaQuery = window.matchMedia('(prefers-color-scheme: light)')

darkModeMediaQuery.addListener(e => {
  const darkModeStatus = e.matches
  console.log(`Dark mode is ${darkModeStatus ? 'on' : 'off'}.`)
})

lightModeMediaQuery.addListener(e => {
  const lightModeStatus = e.matches
  console.log(`Light mode is ${lightModeStatus ? 'on' : 'off'}.`)
})

结合 CSS 变量就可以很好地去适配网站的深色模式了。举个简单示例:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <link rel="stylesheet" href="theme.css" />
  </head>
  <body>
    <div class="container">
      <article>
        <h1>蒹葭</h1>
        <p>蒹葭苍苍,白露为霜。所谓伊人,在水一方。溯洄从之,道阻且长。溯游从之,宛在水中央。</p>
        <p>蒹葭萋萋,白露未晞。所谓伊人,在水之湄。溯洄从之,道阻且跻。溯游从之,宛在水中坻。</p>
        <p>蒹葭采采,白露未已。所谓伊人,在水之涘。溯洄从之,道阻且右。溯游从之,宛在水中沚。</p>
        <p></p>
      </article>
    </div>
  </body>
</html>
/* theme.css */
:root {
  --theme-bg: #fff;
  --theme-color: #000;
}

@media screen and (prefers-color-scheme: dark) {
  :root {
    --theme-bg: #000;
    --theme-color: #fff;
  }
}

body {
  color: var(--theme-color);
  background: var(--theme-bg);
}

以上示例会自适应系统主题色,如果你想交由用户决定,去掉示例中的媒体查询,然后通过 JavaScript 去动态设置 CSS 变量即可。另外,可以看下张鑫旭大佬这篇文章:几行CSS让整站支持深色模式的探索与拓展

兼容性如下,看着还不错:

私认为深色模式的适配难点并不在实现上,通过本文的学习,可以说是没有难度对吧,难点应该在于设计师要考虑全站的交互样式,并制定出深浅模式下的各级色调等。

推荐看下这篇文章:prefers-color-scheme: Hello darkness, my old friend

未完待续...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345