这篇文章你将学到以下内容:
- CSS 变量
- CSS 常用函数
- iPhone X 系列机型适配
- CSS At-rules 和媒体查询
- 深色模式适配
一、简述
CSS 变量(CSS Variables),也称作 CSS 自定义属性(CSS Custom Properties),它是带有前缀 --
属性名,且带有值的自定义属性。然后通过 var
函数在全文范围复用。
1.1 语法
定义 CSS 变量的语法非常简单,在变量名称之前添加两个短横线 --
:
--<custom-property-name>: <declaration-value>
其中 <custom-property-name>
表示变量名称,<declaration-value>
表示变量值,形如:--*
。这类自定义 CSS 属性与 color
、font-size
、background-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.wxss
的 page
内声明。
: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
(默认)、contain
、cover
三种属性值(详见):
<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-left
和safe-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-left
、safe-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 可以归为一类:条件规则组。
这些规则组所指的条件总等效于
true
或false
,如果为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
,此时规则内声明的样式就会生效。
- 多个条件:使用
not
、and
、or
操作符组合。
相当于 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 同样是可以使用的,后者用得较少。
媒体查询可以这样使用:
- 在 CSS 中使用
@media
来装饰样式。- 在 HTML 中将
media
属性作用于<style>
、<link>
、<source>
等元素指定特定的媒体类型。- 在 JavaScript 中使用
Window.matchMedia()
和MediaQueryList.addListener()
方法来测试和监控媒体状态。
需要注意的是,当媒体查询作用于 <link>
元素时,即使媒体查询结果为 false
,该样式表仍会被下载,只是样式内容不被作用而已。
3.2.1 媒体查询语法
每条媒体查询语句(不区分大小写),由一个可选的「媒体类型」和任意数量的「媒体特征」表达式构成。同时可使用多种「逻辑操作符」合并多条媒体查询语句。
当媒体查询语句中指定的媒体类型与设备匹配时,这条媒体查询结果返回 true
。若是多条媒体查询语句组合,需每个条件均为 true
,那么媒体查询结果才为 true
。
3.2.2 媒体类型(Media Types)
在媒体查询中,媒体类型(Media Types)是可选的,默认是 all
类型。一般是使用了 not
或 only
逻辑操作符,才需要显式指定。有以下这些:
媒体类型 | 描述 |
---|---|
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 各级中新增或弃用的媒体特性,可看这里。
对于表示「范围」的的媒体特性(比如 width
、height
等),可以使用前缀 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-height
与height
同理。
3.2.5 逻辑操作符(Logical Operators)
通过逻辑操作符(and
、not
、only
),可以创建复杂、多条件的媒体查询。其中 and
、not
、only
分别表示与、非、唯一的意思。
此外,还可以使用 ,
(逗号)将多个媒体查询合并,只要任一查询结果为 true
,整个查询语句就返回 true
(跟逻辑或 or
行为类似)。
需要注意的是:
not
操作符不能用于否定单个查询条件,只能用于否定整个媒体查询。- 使用
not
、only
操作符,必须指定「媒体类型」。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 应用场景
- 浅色模式与深色模式
可以使用媒体特性 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
未完待续...