CSS 变量使用详解

配图源自 Freepik

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

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

一、简述

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

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

1.1 语法

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

--: 

其中 表示变量名称, 表示变量值,形如:--*。这类自定义 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 选择器相同。

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

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

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

比如

内使用 color: var(--theme-primary),生效的将会是 color: #00f。再者,以下示例中,在
中引用 --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([, ])
  • 表示自定义属性名
  • (可选)表示声明值(后备值),仅自定义属性没有定义时,它才会有效(类似 ES6 中的函数参数设定默认值)。

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

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

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 变量值不合法并不能使得 声明值生效,它仅限于 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 变量值,最终生效的是其继承值,而不是默认值。比如:



  

字体会是什么颜色呢?

你看最终

生效的 color 是其从 中继承过来的 #f00 红色,而不是 color 的默认颜色 canvastext。

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

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

2.1.2 var 函数的尾随空格



  

字号是多大呢?

猜一下 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))
}

注意点:

  • 对于 +- 运算,运算符两边必须要有「空格」,而 */ 运算则没有要求,因此建议都加上空白符。
  • 对于 * 运算,参与运算的至少有一个数值(),且不能为 0
  • 对于 / 运算,运算符 / 右侧必须是一个数值()。
  • 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 三种属性值(详见):


属性值 描述
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 系列刘海屏手机,有以下示例:



  
    
    
    
    Document
    
  
  
    

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

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

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

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



  
    
    
    
    Document
    
  
  
    

蒹葭

蒹葭苍苍,白露为霜。所谓伊人,在水一方。溯洄从之,道阻且长。溯游从之,宛在水中央。

蒹葭萋萋,白露未晞。所谓伊人,在水之湄。溯洄从之,道阻且跻。溯游从之,宛在水中坻。

蒹葭采采,白露未已。所谓伊人,在水之涘。溯洄从之,道阻且右。溯游从之,宛在水中沚。

横屏、竖屏显示如下:

考虑 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) {
    /* ... */
}


// 如果参数是 CSS 声明(也就是出现了冒号),外面需要有个括号,否则语法不正确。
if (window.matchMedia('(max-width: 480px)').matches) {
    // ...
}

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

媒体查询可以这样使用:

  • 在 CSS 中使用 @media 来装饰样式。
  • 在 HTML 中将 media 属性作用于