现代 CSS

图解CSS:CSS自定义属性

特别声明:如果您喜欢小站的内容,可以点击申请会员进行全站阅读。如果您对付费阅读有任何建议或想法,欢迎发送邮件至: airenliao@gmail.com!或添加QQ:874472854(^_^)

众所周之,CSS的维护一直是件不易的事情,特别是在构建大型Web站点或Web应用程序时,如果是多人协作的话难度更大。另外,由于CSS语言是一种声明式语言,而且不像其他语言有变量、条件和逻辑等特性,一直生存在程序语言鄙视链的最底层。也因为这个原因,社区中有了各种CSS处理器语言,比如Sass、LESS和Stylus等。这些处理器语言引入了一些类似其他程序语言的特性,比如变量、运算符和逻辑运算等。

虽然CSS处理器给编写和维护CSS带来了一些便利,但还是需要额外的编译。不过处理器中的变量着实为我们带来很大的优势,也正因为如此,社区开始将CSS处理器中的变量引入到原生CSS中,经过多年的推进和演变才有了今天的CSS自定义属性。

接下来在这一章中,将和大家一起探讨CSS自定义属性。

CSS 自定义属性简介

CSS自定义属性已进入到W3C规范的 TR阶段,纳入在一个独立的模块中,即 CSS Custom Properties for Cascading Variables Module Level 1。 该模块引入了一系列作者(CSSer)自己定义的属性,这些属性统称为自定义属性,允许作者自由的选择名称,自由的为名称属性分配任意值。这些属性能够提供给var()函数使用,被var()函数引用的自定义属性又常被称为变量

这样一来,CSSer声明的这些自由属性就有了两个名称:自定义属性变量

  • 自定义属性:使用--**代表任意声明的名称)声明的特殊格式作为名称,该名称被称为自定义属性,同时可以给自定义属性赋予任何值。比如--color: #fff
  • 变量:CSS的var()函数引用的自定义属性被称为变量var()会返回自定义属性所对应的值,同时可以被运用于相应的CSS属性。对应的即是CSS规则中的属性值

用张图来描述他们之间的关系:

CSS自定义属性的作用

如果你使用过任何编程语言,变量这个词(概念)并不会陌生。在一些命令式编程语言中(比如我们前端熟悉的JavaScript),可以通过变量让我们更好的跟踪某些状态。变量是一种符号,关联着一个特定的值,变量的值能随着时间的推移而改变。变量的好处还在于我们可以把值存储在一个地方,然后在需要的地方调用它或者修改它。这样就不用在程序的不同地方为不同的值添加不同的变量:

所有变量更新使用同一个存储地址

而在CSS这种声明式语言,她是缺乏动态性的。也无法做到随着时间而改变的值并不存在,也就没有所谓变量的概念。可事实上,我们又非常的期待CSS也能和其他的编程语言一样,能随着周围环境和开发者的需求做出不断变化。

在CSS自定义属性还没有出现的之前,CSS自身引入了一种层级变量的概念(稍后会提到),从而能够从容应对可维护性的挑战。这就会使得在整个CSS Tree中都可以象征性的引用一个变量。但这种象征性的变量往往不能很好的解决CSS的维护和扩展的问题,同时这种象征性的变量也足以让一些不了解CSS的同学头痛。这是后话,暂且不表。继续回到我们的实际场景中来。

我想不少同学都应该有过构建大型Web网站或Web应用的经历,使用的CSS数量是非常庞大的,并且在很多场合有着大量的重复使用。就拿网站的配色方案来举例,一些颜色在CSS文件中会出现很多次,并被重复使用。当你修改配色方案时,不论是调整某个颜色或完全修改整个配色,都会是一个复杂的问题。如果单纯的依赖全局的查找替换是远远不够,这样的操作难免会出错。

如果使用了CSS的框架,这种情况会变得尤其糟糕,此时如果要修改颜色,则需要对框架本身进行修改。虽然这些框架都有可能引入了Sass这样的CSS处理器帮助我们减少了出错的机会,提高了可维护性的能力,但这种通过添加额外步骤的方式(需要做编译处理),可能会增加系统的复杂性。

CSS自定义属性(CSS变量)的出现,为我们带来了一些CSS处理器的便利,并且不需要额外的编译。在CSS中使用CSS自定义属性的好处和在编写语言中使用变量的好处没有特别的不同之处。W3C规范上有过这样的一段描述:

使用CSS自定义属性使得大文件更易于阅读,因为看起来很随意的值有了一个提示信息的名字,并且编辑这些文件更加简单,更不易于出错。因为你只需要在自定义属性处修改一次,这个修改就会应用到使用该自定义属性的任何地方

简单地说,CSS自定义属性除了提供了更灵活的设置、引用和修改的便利性之外,还具有较强的语义化(这需要你对语义化有足够强的意识,比如primary这样的名称总是要比red这样的名称来得有意义)。这些语义化信息让你的CSS文件变得易读和理解。

为此,可读性和可维护性是CSS自定义属性最大的优势

CSS自定义属性语法和基础应用

在介绍CSS自定义属性时,我们可以从一些熟悉的东西开始着手,这样对于从后端转过来的同学更易于理解。我们就拿JavaScript中的变量来举例吧。

在JavaScript中,声明一个变量有多种方式,比如:

var customProperty

// 或
let customProperty = true

// 还可以
const customProperty = IS_ACTIVE

如果你对JavaScript和我一样的不熟悉,或者说你对CSS处理器有所接触过,我们可以拿CSS的处理器来举例。我们熟悉的几个CSS处理器,比如LESS、Sass和Stylus在声明变量的时候都有着自己的方式。通常会使用一个象征性的实体符来做为变量的前缀,比如说Sass中会使用$符,在LESS中使用@符,Stylus声明变量不带特殊前缀,直接使用表达式,比如primary-color=red

CSS自定义属性在声明的时候也使用了类似的方法,它引入了--符号做为前缀来声明一个自定义属性:

:root {
    --primary: #f36;
}

示例中的--primary就是一个我们所要说的CSS自定义属性。CSS自定义属性和常规CSS属性的用法是一样的。把它们当作动态属性会比变量更好。这意味着它们只能在声明块中使用。也就是说,自定义属性和选择器是强绑定的。可以是任何有效的选择器。

如果已声明的CSS自定义属性未被任何属性调用的话,将不会产生任何的效果。只会是一段字符串停留在你的样式文件中。

调用已声明的CSS自定义属性和其他CSS处理器中变量的调用略有不同。调用CSS自定义属性需要通过var()函数来引用。将CSS自定义属性当作var()函数的第一个参数传进去,并将整个函数赋值给CSS的属性(可以是CSS的属性,也可以是CSS的自定义属性),比如:

body {
    color: var(--primary);
}

.button {
    --primaryButton: var(--primary);
}

示例中的var()函数可以代替元素中任何属性中的值的任何部分。不过var()函数不能作为属性名选择器或者其他除了属性值之外的值

var()函数同时可以接受两个值:

var(<custom-property-name>, <declaration-value>)

其中<custom-property-name>是CSS的自定义属性;<declaration-value>是一个回退值,该值被用来在自定义属性值无效的情况下保证var()函数有值,能让CSS属性规则生效。比如:

:root {
    --primary: #f36;
}

.button {
    background-color: var(--primary, #fff);
    color: var(--color, #333);
}

CSS 自定义属性的使用

现在对CSS自定义属性有了一个基本的认识,接下来用几个小示例代码向大家演示CSS自定义属性的一些特性,从而增强对其认知。

CSS自定义属性和CSS属性工作原理完全相同

CSS自定义属性可以在任何元素、选择器,甚至是伪元素上声明的普通属性。他的使用和CSS属性的使用是相同的,原理也是相同的:

:root {
    --font-size: 1em;
}
p {
    font-size: var(--font-size);
}
section::after {
    font-size:1.5em;
}

CSS自定义属性和CSS属性一样具有继承和级联特性

CSS中有三个概念是学习CSS必须要掌握的,即层叠继承权重。CSS自定义属性同样的具备继承和级联等特性。比如说:

<!-- HTML -->
<div class="parent">
    <div class="child1"></div>
    <div class="child2"></div>
</div>

// CSS
.parent {
    --primary: #f36;
}

.child1 {
    background-color: var(--primary);
}

.child2 {
    color: var(--primary);
}

上面的示例中,.child1.child2都继承了他们父元素.parent中的--primary自定义属性。但在很多情况之下,我们不需要这样行为,我们可以在:root{}中来显式声明自定义属性:

:root {
    --primary: #f36;
}

.child1 {
    background-color: var(--primary);
}

.child2 {
    color: var(--primary)
}

这样可能不易于理解,拿一个更真实的案例来举例。比如说,每个Web应用都会有自己的下色系,就拿Bootstrap这个CSS框架的色系来说吧,它的主色系是--primary: #007bff,该色会用于多个地方,比如:

使用CSS继承的特性,可以把事情变得更简易:

:root{
    --primary: #007bff;
}

// Button组件
.btn-primary {
    background-color: var(--primary);
    border-color: var(--primary);
}

// Badge组件
.badge-primary {
    background-color: var(--primary);
    border-color: var(--primary);
}

// Dropdowns组件
.dropdown-primary {
    background-color: var(--primary);
    border-color: var(--primary);
}

// Pagination组件
.page-link {
    color: var(--primary);
}

// Progress组件
.progress-bar {
    background-color: var(--primary);
}

有朝一日,你的老板说这种颜色不想再看了,想换换色系,那么只需要调整:root中的--primary的值即可。

另外就是相同的模块组件只有略微性的差异,比如下图这样的一个效果:

对于这样的一个效果,借助CSS的级联特性,也可以将事情变得更容易:

:root {
    --color: #333;
}

.card {
    color: var(--card);

    &:nth-child(2) {
        --color: #2196F3;
    }

    &:nth-child(3) {
        --color: #f321ab;
    }
}

因为CSS的级联和继承是较为复杂的,为了更好的能让大家清楚CSS自定义属性使用时借助CSS的级联和继承彰显出自己特性(代码量更少,更易维护,更易扩展)。再为大家展示一个层级更深的示例:

<!-- HTML -->
<p>我是什么颜色?</p> 
<div>我又是什么颜色?</div>
<div id="alert">
    我是什么颜色?
    <p>我又是什么颜色?</p>
</div>

// CSS
:root {
    --color: #333;
}
div {
    --color: #2196F3;
}
#alert {
    --color: #f321ab;
}

结果用下图来阐述:

如果你想对CSS层叠和继承相关的知识做进一步了解的话,可以阅读《图解CSS:CSS层叠和继承》一文。也可以阅读@Miriam的《CSS Custom Properties In The Cascade》一文。

CSS自定义属性可以在行内style属性中使用

CSS自定义属性和CSS属性一样,可以在元素的style属性中使用CSS自定义属性。

<!-- HTML -->
<button style="--color: blue">Click Me</button>

// CSS
button {
    border: 1px solid var(--color);
}

button:hover {
    background-color: var(--color);
}

内联样式中使用CSS自定义属性非常有意义,特别是在通过JavaScript来操作CSS自定义属性的时候特别更有意义。

CSS自定义属性区分大小写

CSS自定义属性和CSS属性略有不同,CSS的属性不会区分大小写,但CSS自定义属性则会区分大小写。

:root {
    --COLOR: #fff;
    --color: #f36;
}

.box {
    color: var(--COLOR;
    background-color: var(--color);
}

CSS自定义属性命名

CSS自定义属性的命名规则比较松散,可以是任何有效的字符,比如中文大写字母驼峰命名中距线emojiHTML实体等等:

CSS自定义属性支持回退参数

在介绍CSS自定义属性的时候,如果将CSS自定义属性当作var()函数的参数传进去的时候,它还支持第二个参数,即回退参数。而CSS的属性是不支持这一特性。

  • 如果CSS自定义属性不被浏览器支持,那么可以提供一个降级的参数以备能让浏览器识别
  • 如果浏览器支持CSS自定义属性,但并没有显式声明该CSS自定义属性的值,则会选择降级的参数
  • 如果浏览器支持CSS自定义属性,而且显式声明了该CSS自定义属性的值,则会选择CSS自定义属性的值,不会选择降级的参数

比如下面这个示例:

:root {
    --color: #f36;
}

.box {
    width: var(--w, 100px);
    color: var(--color, #fff);
    border-width: var(--color, 2px);
}

无效的CSS自定义属性将会发生什么?

无效的CSS自定义属性运用于一个CSS的属性时将会发生什么呢?在告诉大家会发生什么之前,我们先来看看在CSS的属性中使用了一个无效的值会发生什么?

我想我们都有过手误的经过,比如说,在做CR(Code Review)的时候会发现这样的现象,比如:

.card {
    padding: -10px;
}

padding是不支持负值,也就是说,-10px对于padding属性来说是一个无效的值,这个时候,浏览器在渲染的时候会采用padding的初始值(initial),即0

在使用CSS自定义属性的时候,如果CSS自定义属性在被调用时对于CSS属性来说是一个无效值时,也会采用initial做为降级处理,比如:

:root {
    --color: 20px;
}

.p {
    color: var(--color);
}

就上面的示例而言,声明的--color: 20px是一个有效的值,但其被用于color属性时,--color是一个无效的值,因为20px对于color属性而言是一个无效的值。在这种情形之下,color属性会采用其初始值initial(取决于用户代理)。如果该元素的父辈元素没有显式设置color的值,那么会继承<html>元素的color值,在Chrome浏览器下会是一个#000的颜色值。

还有另外一个情景,虽然在调用已声明的CSS自定义属性是一个无效的值,但提供了一个降级值,而且该降级的值是一个有效的值,那么就不会采用initial值,而是会采用降级值,比如:

:root {
    --color: 20px;
}

p {
    color: var(--color, blue);
}

链式的CSS自定义属性

使用var()函数调用已声明的CSS自定义属性时,给var()提供降级参数时,我们可以使用链式的方式提供降级参数,比如:

p {
    --color1: red;
    --color2: blueviolet;
    --color3: orange;

    color: var(--color1, var(--color2, var(--color3, blue)));
}

循环依赖的CSS自定义属性是无效的

CSS是一门声明性的语言,元素的样式规则没有顺序的概念(相同的属性出现在同一个选择器块内,后者会覆盖前者)。它只能有一个值,它不可能同时是以前值和它的值加1,因此这形成了一个循环。

我们先从JavaScript中来讲起,比如:

var a = 1;
var a = a;
console.log(a); // » 1

我们再来看CSS自定义属性的循环使用:

:root {
    --size: 10px;
    --size: var(--size);
}

body {
    font-size: var(--size, 2rem);
}

在CSS中,相同的CSS属性在同一个选择器块内,后者会覆盖前者,比如上面的示例中--size: var(--size)将会覆盖--size。但CSS自定义属性如果其值依赖于自身的话,即,它使用的是引用自身的var(),该值是无效的。上例中--size自定义属性无效,在body中调用--size时无效,此时采用了var()的降级值2rem

除了自定义属性引用自身外,还有另外一情景,那就是两个或多个自定义属性之间相互引用:

:root {
    --one: calc(var(--two) + 10px);
    --two: calc(var(--one) - 10px);
}

这种相互引用的CSS自定义属性也是无效值。目前唯一可破的方式是:不要在代码中创建具有循环依赖关系的CSS自定义属性

带基本运算符的CSS自定义属性

熟悉CSS处理器的同学应该都知道,CSS处理器中可以使用运算符相关的操作。该特性其实在CSS中借助calc()函数也可以做一些基本运算符相关的操作。在CSS自定义属性中具有同样的特性:

:root {
    --indent-size: 10px;

    --indent-xl: calc(var(--indent-size) * 2);
    --indent-l: calc(var(--indent-size) + 2px);
    --indent-s: calc(var(--indent-size) - 2px);
    --indent-xs: calc(var(--indent-size) / 2);
}

p {
    text-indent: var(--indent-xl);
}

在CSS自定义属性中进行基本运算符操作的时候,需要注意的一点就是不能计算不带单位的值

:root {
    --gap: 10;
}

.card {
    padding: var(--gap)px 0; /* 无效,不能正常运行 */
    padding: calc(var(--gap) * 1px) 0; /* 有效,可以正常运行 */
}

CSS自定义属性的值是一个数据而不是一个属性值

上面的示例我们看到了,可以给CSS自定义属性赋值一个纯数字的值,而不是一个CSS的属性值。这样做有一个较好的优势,那就是可以方便的将其变成我们想要的值。比如:

.block {
    --size: 40;
    width: var(--size)vw;
}

前面提到了,这种操作是一个无效值(var(--size)vw)。前面说过,当CSS自定义属性无效时,CSS属性的值将会采用该属性的初始值,比如上面示例中的width会采用auto的值。由于浏览器将所有内容都考虑为令牌(Tokens)形式,所以它将--size值设置为numbervw设置为identifier,因此会把上面示例中的var(--size)vw解析为40 vw(中间有空格),而不是我们期待的40vw(最终采用的是width的初始值auto)。

我们可以使用calc()函数来解决这个问题:

.block {
    --size: 40;
    width: calc(var(--size) * 1vw);  // => width: 40vw
    height: calc(var(--size) * 1vh); // => height: 40vh
}

很多同学在编码的时候都喜欢追求极致,可能希望通过--size:40vw来节省至少一个calc()函数的计算,比如:

.block {
    --size: 40vw;
    width: var(--size);
    height: calc(var(--size) / 1vw * 1vh);
}

希望通过vw / vw的方式去除vw单位,将--size变成纯数字,然后乘以1vh之后变成height: 40vh。但结果并不是我们期待的一样,那是因为calc()并不支持除以带长度单位的值(比如pxvwvh等),calc()仅支持除以纯数字,所以height属性引用了一个无效的值,而采用其初始值,即auto

calc()不支持除以带单位的值,所以只能除以数字。

calc()和CSS自定义属性之间的基本运算,主要还是依赖于calc()函数的规则:

  • 如果自定义属性是一个纯数字,calc()可以把它转换成任何单位的值,只需要把CSS自定义属性的值乘以1和那个单位,比如calc(var(--size) * 1px),将会生成40px
  • 如果自定义属性带有一个单位,calc()就没有办法将它转换成一个纯数字(也许使用JavaScript可以实现)
  • 如果自定义属性带有一个单位,calc()可以乘以或除以不带任何单位的一个纯数字
  • 如果自定义属性带有一个单位,calc()可以加减任何一个带单位的数值

CSS自定义属性在@规则中的使用

CSS有一些@规则,比如@charset@import@namespace@document@font-face@keyframes@media@page@supports@viewport@color-profile等。其中@media@supports@keyframes又是我们常见的@规则。

@media@supports是可用于CSS的条件判断(详见条件CSS);@keyframes用于CSS的animation。接下来,我们主要来看看CSS自定义属性在@media@supports@keyframes中的使用。

CSS自定义属性在@media中的使用

CSS自定义用于媒体查询@media中,可以动态更变CSS自定义属性的值,比如:

body {
    --color: #f36;
    background-color: var(--color);
}

@media screen and (max-width: 375px) {
    body {
        --color: #9f3;
    }
}

效果如下:

上面演示的示例是最简单的一种,事实上,CSS自定义属性和@media的结合使响应式Web设计变得更简单。

CSS自定义属性在@keyframes中的使用

CSS自定义属性和CSS动画可以很好的一起工作。不过一定要在动画元素中显式声明CSS自定义属性,并在@keyframes中使用var()来引用声明好的CSS自定义属性。这样做的好处是,可以修改目标选择器中的CSS自定义属性,而不需要在@keyframes中查找每个属性。

.animate {
    --from-color: red;
    --to-color: lime;
    width: 100px;
    height: 100px;
    animation: blink 1s infinite;
}

@keyframes blink {
    from {
        background-color: var(--from-color);
    }
    to {
        background-color: var(--to-color);
    }
}

效果如下:

CSS自定义属性在@supports中的使用

到写这篇文章为止,浏览器对CSS自定义属性的支持已经非常好了,几乎超过90%

我们可以在一定程度上使用@supports来做条件判断:

body {
    background-color: #f4f4f4;
}

@supports(--css:variables) {
    body {
        --bg-color: linear-gradient(to bottom, #f36, #f4f4f4);
        background: var(--bg-color);
    }
}

CSS自定义属性在Web组件中的应用

Web组件现在在Web网站或Web应用程序开发过程中得到普遍的运用,而且很多团队或个人都有做相关的沉淀。其实在开发或设计Web组件时,把CSS自定义属性结合进来,会让整个设计变得更为灵活。

我们拿社区中最有影响力的CSS FrameworkBootstrap)的ButtonUI组件来举例,向大家阐述CSS自定义属性在Web组件中的应用。

先来看其HTML结构

<!-- HTML -->
<button type="button" class="btn btn-primary">Primary</button>
<button type="button" class="btn btn-secondary">Secondary</button>
<button type="button" class="btn btn-success">Success</button>
<button type="button" class="btn btn-danger">Danger</button>
<button type="button" class="btn btn-warning">Warning</button>
<button type="button" class="btn btn-info">Info</button>
<button type="button" class="btn btn-light">Light</button>
<button type="button" class="btn btn-dark">Dark</button>
<button type="button" class="btn btn-link">Link</button>

可以看到,每个按钮都有两个类名,一个是基本类名.btn;另一个是扩展类名,比如.btn-primary。其样式:

.btn {
    display: inline-block;
    font-weight: 400;
    color: #212529;
    text-align: center;
    vertical-align: middle;
    user-select: none;
    background-color: transparent;
    border: 1px solid transparent;
    padding: .375rem .75rem;
    font-size: 1rem;
    line-height: 1.5;
    border-radius: .25rem;
    transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}

.btn:hover {
    color: #212529;
    text-decoration: none;
}

.btn:focus {
    outline: 0;
    box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
}

.btn-primary {
    color: #fff;
    background-color: #007bff;
    border-color: #007bff;
}

.btn-primary:hover {
    color: #fff;
    background-color: #0069d9;
    border-color: #0062cc;
}

.btn-primary:focus {
    box-shadow: 0 0 0 0.2rem rgba(38,143,255,.5);
}

.btn-primary:not(:disabled):not(.disabled):active {
    color: #fff;
    background-color: #0062cc;
    border-color: #005cbf;
}

.btn-primary:not(:disabled):not(.disabled):active:focus {
    box-shadow: 0 0 0 0.2rem rgba(38,143,255,.5);
}

对于另外UI风格的按钮,通过扩展类名来调整,比如.btn-danger

.btn-danger {
    color: #fff;
    background-color: #dc3545;
    border-color: #dc3545;
}

.btn-danger:hover {
    color: #fff;
    background-color: #c82333;
    border-color: #bd2130;
}

.btn-danger:focus {
    box-shadow: 0 0 0 0.2rem rgba(225,83,97,.5);
}

.btn-danger:not(:disabled):not(.disabled):active {
    color: #fff;
    background-color: #bd2130;
    border-color: #b21f2d;
}

.btn-danger:not(:disabled):not(.disabled):active:focus{
    box-shadow: 0 0 0 0.2rem rgba(225,83,97,.5);
}

将上面的代码拆分成两部分,HTML结构和按钮基本样式:

另外一部分就是扩展样式,拿primarydanger作为示例:

从上面的拆分出来的示例代码来看,不同的风格的按钮具有相同的CSS属性,只是值不同,比如colorbackground-colorborder-colorbox-shadow(颜色)。这样一来,我们可以将这些相同的CSS属性抽取出来成为CSS自定义属性。比如:

.btn {
    --color: `#212529`;
    --background-color: transparent;
    --border-color: transparent;
    --box-shadow-color: rgba(0, 123, 255, .25);

    display: inline-block;
    font-weight: 400;
    text-align: center;
    vertical-align: middle;
    user-select: none;
    padding: .375rem .75rem;
    font-size: 1rem;
    line-height: 1.5;
    border-radius: .25rem;
    transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out;

    color: var(--color);
    background-color: var(--background-color);
    border: 1px solid var(--border-color);
}
.btn:hover {
    --color: #212529;
    text-decoration: none;
}
.btn:focus {
    outline: 0;
    box-shadow: 0 0 0 .2rem var(--box-shadow-color);
}

这样我们在改变按钮样式风格的时候,就会显得简易地多:

.btn-primary {
    --color: #fff;
    --background-color: #007bff;
    --border-color: #007bff;
}

.btn-primary:hover {
    --background-color: #0069d9;
    --border-color: #0062cc;
}

.btn-primary:focus {
    --box-shadow-color: rgba(38, 143, 255, .5);
}

.btn-primary:not(:disabled):not(.disabled):active {
    --background-color: #0062cc;
    --border-color: #00fcbf;
}

.btn-primary:not(:disabled):not(.disabled):active:focus {
    --box-shadow-color: rgba(38, 143, 255, .5);
}

有关于CSS自定义属性在Web组件中更详细的介绍,可以阅读《CSS 自定义属性在Web组件中的应用》一文。

CSS自定义属性在SVG中的应用

CSS自定义属性和SVG结合在一起可以很好的工作。

可以使用CSS自定义属性来修改内联SVG中的样式和展示性相关的属性

比如在不同地方使用相同的图标,只是颜色不同时,就可以在图标的容器元素上设置CSS自定义属性,并赋予不同的属性值。比如:

<svg xmlns="http://www.w3.org/2000/svg" class="icon-container">
    <symbol id="icon-close" viewbox="0 0 1024 1024">
        <path fill="var(--icon-color)" d="M512 1023.089778C230.172444 1023.089778 0.896 793.813333 0.896 512S230.172444 0.910222 512 0.910222 1023.089778 230.172444 1023.089778 512c0 281.813333-229.319111 511.089778-511.104 511.089778z m0-971.093334C258.318222 51.996444 51.968 258.360889 51.968 512c0 253.653333 206.364444 460.003556 460.003556 460.003556 253.639111 0 459.975111-206.364444 459.975111-460.003556 0-253.653333-206.307556-460.003556-459.975111-460.003556zM692.366222 717.937778a25.457778 25.457778 0 0 1-18.076444-7.495111L511.815111 547.982222 349.653333 710.4a25.528889 25.528889 0 1 1-36.053333-36.053333L476.017778 511.857778 313.543111 349.681778a25.557333 25.557333 0 0 1 36.124445-36.124445L512.142222 476.003556l162.161778-162.446223a25.528889 25.528889 0 1 1 36.053333 36.053334L547.982222 512.099556l162.474667 162.204444a25.557333 25.557333 0 0 1-18.033778 43.648h-0.028444z" />
    </symbol>
</svg>

使用<symbol>标签可以创建内联的SVG Sprites(只不过该示例只使用了个关闭图标)。然后可以使用<use>xlink:href引用<symbol>中的id值:

<svg role="img">
    <use xlink:href="#icon-close" />
</svg>

在SVG的<symbol>创建的图标中有关于图标的样式型属性使用CSS自定义属性来设置,比如fill="var(--icon-color)"。这样我们在使用的时候就可以和前面介绍的Button类似,调整--icon-color的值,就可以得到不同颜色的图标,比如:

整个效果如下:

使用CSS自定义属性模仿不存在的CSS规则

CSS自定义属性还可以让我们更好的模仿不存在的CSS规则,比如box-shadow-colortext-shadow-colorbackground-sprites等。就拿CSS的box-shadow来举例,CSS的box-shadow有一个阴影颜色,但在CSS中是没有独立的一个属性来设置阴影颜色的,这个时候CSS自定义属性就可以派上用场了:

.box {
    --box-shadow-color: rgba(0,0,0,.25);
    width: 100px;
    height: 100px;
    bakcground-color: #fff;
    box-shadow: .25em .25em .5em var(--box-shadow-color);

    &:hover {
        --box-shadow-color: rgba(120, 220, 250, .25);
    }
}

在鼠标悬浮的时候,更改--box-shadow-color就改变了阴影颜色,相当于模仿了一个阴影颜色的属性:

再来看一个复杂一点的案例,模拟background-sprites

.sprites{
    --sprites-color: rgba(243, 17, 17, 0.4);
    --background-sprites: linear-gradient(-45deg, var(--sprites-color) 25%, transparent 25%, transparent 50%, var(--sprites-color) 50%, var(--sprites-color) 75%, transparent 75%, transparent);

    background-image: var(--background-sprites);
    background-size: 35px 35px;
    width: 50vw;
    height: 35px;
    border: 1px solid #ccc;
    border-radius: 5px;
    margin: 10px;

    &:hover {
        --sprites-color: #57aed1;
    }
}

使用JavaScript操作CSS自定义属性

CSS自定义属性和CSS属性一样,可以通过CSSOM中的一些API来操作CSS自定义属性:

  • 使用.style.getPropertyValue(--foo)从内联样式中获取CSS自定义属性
  • 使用getComputedStyled(element).getPropertyValue(--)从任何地方获取CSS自定义属性
  • 使用.style.setProperty('--foo', 'red')将CSS自定义属性设置为内联样式

前两者是用来获取CSS自定义属性,后者用来设置CSS自定义属性。

在实际上,使用CSS自定义属性和其几个简单的CSSOM API结合在一起,可以轻易地实现一些动效。拿@Val Head在CodePen上的一个案例来举例:

HTML结构很简单:

<!-- HTML -->
<div class="ball">
    <div class="halo"></div>
    <div class="halo"></div>
    <div class="halo"></div>
</div>

:root中显式声明了几个CSS自定义属性:

:root {
    --mouse-x;      // » 鼠标 x 轴坐标值
    --mouse-y;      // » 鼠标 y 轴坐标值
    --scale;        // » 缩放值
    --radius: 40px; // » 圆角半径
    --factor: 1;    // » 缩放因子
}

基本样式:

.ball {
    background: #D92659;
    border-radius: 50%;
    position:absolute;

    width: var(--radius);
    height: var(--radius);
    transform: translate(calc(var(--mouse-x) * 1px - var(--radius)/2),calc(var(--mouse-y) * 1px - var(--radius)/2));
}

.halo {
    background: rgb(114, 61, 83);
    border-radius: 50%;
    position:absolute;
    opacity: .15;

    width: var(--radius);
    height: var(--radius);
    filter: blur(var(--factor));
    transform: scale(calc(var(--scale) * var(--factor)));
}

.halo:nth-of-type(1) {
    --factor: .3;
}

.halo:nth-of-type(2) {
    --factor: 0.5;
}

.halo:nth-of-type(3) {
    --factor: .9;
}

在JavaScript中添加几行简单的代码来控制声明的CSS自定义属性:

var [xpos,targetX,ypos,targetY, velX, velY] = [0,0,0,0,0,0];

const docStyle = document.documentElement.style;
const drag = 0.8;
const strength = 0.12;

function springItOn() {

    var diffX = targetX - xpos; 
    diffX *= strength;
    velX *=drag;
    velX += diffX;
        xpos += velX;

    var diffY = targetY - ypos; 
        diffY *= strength;
        velY *=drag;
        velY += diffY;
        ypos += velY;

    docStyle.setProperty('--mouse-x', xpos);
    docStyle.setProperty('--mouse-y', ypos);

    docStyle.setProperty('--scale', (velY + velX)*strength);

    requestAnimationFrame(springItOn);
}

springItOn();

// 跟随鼠标的坐标值来更新目标元素的位置
document.addEventListener('mousemove', (e) => {
    targetX = e.clientX;
    targetY = e.clientY;
});

这个时候你在屏幕上移动鼠标,球体也会跟着移动,而且带有缩放,光晕等效果:

再给大家提供一个@Hubert Souchaud写的示例:

如果你对这方面的知识感兴趣的话,还可以阅读早期整理的《CSS自定义属性制作动画》一文。

使用CSS自定义属性来改变我们构造CSS方式

CSS自定义属性有可能改变我们编写和思考CSS的方式

CSS自定义属性的出现,让我们可以编写更干净,更紧凑,更灵活的CSS,甚至它有可能改变我们编写和思考CSS的方式。接下来我们通过几个小点来和大家一起讨CSS自定义属性如何改变我们构造CSS方式

从设计中分离逻辑

主要的优势是我们现在有能力完全从设计中分离逻辑。这实际上意味着将CSS自定义属性声明和属性的声明分离。

/* CSS自定义属性声明 */
:root {
    --my-var: red;
}

/* CSS属性声明 */
body {
    background-color: var(--my-var);
}

在CSS处理器中我们就一直提倡应该将变量声明和CSS属性声明分开。在CSS自定义属性的实践中,这一点不应该改变。我们在声明CSS自定义属性和CSS属性的时候也应该分开。

折叠逻辑

在文档或函数顶部声明自定义属性的想法是很早就有的方式。也是大多数语言中推荐的做法,现在我们也可以在CSS这样做。以这种方式来编写CSS,光从视觉角度就很容易区分顶部和之后的代码。我要使用它们时我能很方便的找到它们。这就是所谓的 “折叠逻辑”

在这个折叠的上方包含所有CSS自定义属性。这样就更容易地知道自定义属性发生了哪些变化,也让CSS代码可读性更高。来看一个小示例:

.row  {
    --row-display: block;
}

@media screen and (min-width: 30em) {
    .row {
        --row-display: flex;
    }
}

在折叠区域下面的代码可以看起来像这样:

.row {
    display: var(--row-display);
    flex-direction: row;
    flex-wrap: nowrap;
}

更改值而不是CSS自定义属性

在大多数情况下,如果媒体查询或CSS选择器将一个CSS自定义属性替换为另一个CSS自定义属性,但这并不是一种好的方式。我认为与其交互换CSS自定义属性,不如定义一个CSS自定义属性,设置它的初始值并使用选择器或媒体查询来更改它。

如果它改变了,它就是一个CSS自定义属性

我相信在大多数情况下,响应式设计逻辑都应该包含在CSS自定义属性中。还有一个强有力的论据,当更改任何值时,无论是在媒体查询还是元素范围内,它都属于一个自定义属性。如果它改变了,根据定义它是一个CSS自定义属性,并且这个逻辑应该从设计中分离出来。

更少的媒体查询

所有与CSS自定义属性相关的逻辑都放在文档的顶部(即:root)是有意义的。它更容易维护,因为你可以在一个地方更改它,而且更易阅读。因为你可以在不阅读整个样式表的情况下就可以看到正在更改的内容。

我们不能对媒体查询这样做,因为它将样式表不同部分的元素样式规则分割出来。这既不实用也不可维护,因此使用与所更改的相同选择器相关的声明对媒体查询进行分组是有意义的。

CSS自定义属性在提供了逻辑和设计实现之间的链接。这也意味着,在大多数情况下,除了更改CSS自定义属性之外,不应该要求使用媒体查询,这些CSS自定义属性应该位于带有CSS自定义属性声明的文档的顶部。在“逻辑折叠”之上。

简化选择器

有效地将逻辑与设计分离还可以避免主要属性声明的复杂性,从而可以组合选择器。

在下面的示例中,我们有一个<aside><main>元素,它们具有不同的font-size。同时<aside>有一个黑色的背景色,<main>有一个亮色的背景色。那么我们可以这样来写:

/* 默认值 */
:root {
    --font-size: 1rem;
    --background-color: #fff;
    --color: #333;
}

/* aside元素内的值 */
aside {
    --font-size: 1.2rem;
    --background-color: #333;
    --color: #fafafa;
}

/* 属性的声明 */
main,
aside {
    font-size: var(--font-size);
    color: var(--color);
    background-color: var(--background-color);
}

尽管外观上完全不同,但这两个元素具有完全相同的属性声明。

更通用的CSS自定义属性

你可能会有一个这样的想法,在通用选择器*中声明所有处理逻辑相关的CSS自定义属性。事实上,这样的做法是不好的:

/* 千万不要这样做 */
* {
    display: var(--display);
    width: var(--width);
    height: var(--height);
    border: var(--border);
    background: var(--background);
    ...
}

虽然这么做有趣,但是我们应该更注意重复使用的CSS自定义属性和组合选择器。CSS自定义属性受级联影响。我们可以在.container中这样来设置border

.container {
    --border: 2px solid #ccc;
}

容器内的所有内容都将继承--border。很快,你将覆盖所有的CSS自定义属性,并且不需要一个通用*选择器,把自己带入坑中。

CSS自定义属性和CSS处理器变量的差异

从表面上看,很多同学都认为CSS自定义属性和CSS处理器中的变量有点类似,但事实上它们之间还是有很大的差别。

语法上的差异

CSS自定义属性有点像CSS处理器中的变量,但还是有很大的差别。最重要的也是最明显的区别是语法上。比如在SCSS中我们使用$来声明变量,而且不需要在代码块中{}中声明。

$color: red;

而CSS自定义属性使用--前缀来声明,并且需要在一个选择器块内声明,比如:

:root {
    --color: red;
}

而且在引用上也有明显的差异,SCSS引用已声明的变量,采用的是“键值对”的语法规则,自定义属性需要通过var()函数去取值:

// SCSS
body {
    color: $color;
}

// CSS自定义属性
body {
    color: var(--color);
}

另外一个明显的区别是名称。它们之所以被称为自定义属性,是因为它们是纯粹的CSS属性。在CSS处理器中,你可以在任何位置声明变量,包括外部声明块,在媒体查询中,甚至在选择器中也可以使用,比如:

// SCSS
$breakpoint: 800px;
$color: #d33a2c;
$list: ".text, .cats";

@media screen and (min-width: $breakpoint) {
    #{$list} {
        color: $color;
    }
}

自定义属性和常规CSS属性的用法是一样的。把它们当作动态属性会比变量更好。这意味着它们只能在声明块中使用。换句话说,自定义属性和选择器是强绑定的。前面也提到过了,可以是:root选择器,也可以是任何有效的CSS选择器。

:root {
    --color: red;
}

@media screen and (min-width: 800px) {
    .text,
    .cats {
        color: var(--color);
    }
}

你可以在属性声明中的任何地方获取CSS自定义声明的值,这个意味着它们可以作为单个值使用,作为一个简写语句的一部分,甚至是在calc()函数中使用:

.cats {
    color: var(--color);
    margin: 0 var(--margin-horizontal);
    padding: calc(var(--margin-horizontal) / 2);
}

但是CSS自定义属性不能用于媒体查询或选择器,包括:nth-child()这样的结构性选择器:

/* 下面这样的用法是无效的 */
:root {
    --num: 2;
    --breakpoint: 30em;
}

div:nth-child(var(--num)) {
    color: var(--color)
}

@media screen and (min-width: var(--breakpoint)) {
    :root {
        --color: green;
    }
}

动态 vs. 静态

CSS处理器运行机制的过程可能大致如下图:

CSS处理器中的代码最终需要在页面上给大家呈现的依旧是CSS代码。也就是说,CSS处理器中的变量或其他功能仅仅是在编译的时候生效,它是一种静态的。而CSS自定义属性却不一样,他是一种动态的,你在客户端运行时就可以做出相应的改变。比如,在不同的断点运行时,.card的间距不同。

在CSS处理器,我们可能会这样来做,拿SCSS举例吧:

// SCSS
$gutter: 1em;

@media screen and (min-width: 30em) {
    $gutter: 2em;
}

.card {
    margin: $gutter;
}

使用过CSS处理器的同学都知道,@media中的$gutter并没有生效,最后编译出来的CSS代码中,只能找到:

.card {
    margin: 1em;
}

不管浏览器宽度怎么变化,$gutter的值始终都是1em。这就是所谓的静态(处理器无法在客户端动态编译)。其主要原因是CSS处理器需要经过编译之后才能在客户端运行,而CSS自定义属性却不需要经过编译这一环节,可以在客户端上直接使用,比如:

:root {
    --gutter: 1em;
}

@media screen and (min-width: 30em) {
    :root {
        --gutter: 2em;
    }
}

.card {
    margin: var(--gutter);
}

就上面示例代码,当你改变浏览器窗口的时候,你会发现.cardmargin会发生相应的变化。即,CSS自定义属性是动态的(可以在客户端动态响应)

另外,我们=无法使用JavaScript来动态修改SCSS的变量(其他CSS处理器中的变量也是一样的)。但CSS自定义属性却不同,我们可以通过CSSOM的API来动态获取或修改CSS自定义属性的值。我们在前面探讨CSS自定义属性的基本使用时,也有接触过。比如让元素随着鼠标移动来改变位置:

:root {
    --mouse-x;
    --mouse-y;
}

.move {
    left: var(--mouse-x);
    top: var(--mouse-y)
}

let moveEle = document.querySelector('.move');
let root = document.documentElement;

moveEle.addEventListener('mousemove', e => {
    root.style.setProperty('--mouse-x', `${e.clientX}px`);
    root.style.setProperty('--mouse-y', `${e.clientY}px`);
})

级联和继承

CSS处理器和CSS自定义属性还有一个较大的差异性,那就是级承和继承方面的。通过前面的探讨,我们知道CSS自定义属性是具备CSS属性的级联和继承相关的特性。但在CSS处理器中是不具备这方面的特性。先来看级联方面的特性:

// SCSS
$font-size: 1em;

.user-setting-larger-text {
    $font-size: 1.5em;
}

body {
    font-size: $font-size;
}

// CSS自定义属性
:root {
    --font-size: 1em;
}

.user-setting-large-text {
    --font-size: 1.5em;
}

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

上面的示例中,SCSS编译出来的CSS代码,bodyfont-size始终都是1em,哪怕是用户显式设置了.user-setting-large-text也是如此。但在CSS自定义属性中却不一样,默认bodyfont-size值是1em,一旦.user-setting-large-text生效(比如说显式在body中添加了这个类名或JavaScript给body添加了这个类名),那么bodyfont-size值就变成了1.5em

再来看一个继承方面的示例。拿一个alert(警告框)的UI为例。很多时候我们希望某些元素的UI能继承父元素的值或在其基础上做相应的计算,比如下面这样的一个示例:

// SCSS
$alert-color: red;
$alert-info-color: green;

.alert {
    background-color: $alert-color;

    &.info {
        background-color: $alert-info-color;
    }

    button {
        border-color: darken(background-color, 25%);
    }
}

// CSS 自定义属性
.alert {
    --background-color: red;
    background-color: var(--background-color)
}

.alert.info {
    --background-color: green;
}

.alert button {
    border-color: color-mod(var(--background-color), darken(25%));
}

作用域:全局 vs. 局部

这里先简单的做一个抛砖引玉,后面我们将会花一节内容专门来介绍CSS自定义属性的作用域。

CSS处理器中比如SCSS,变量有两种类型:本地(local全局(global。要任何选择器或构造器声明的变量是全局变量,否则是本地变量。

任何潜套的代码块都可以访问封闭内的变量:

$globalVar : 10px; // Global variable
.enclosing {
    $enclosingVar: 20px; // Local variable

    .closure {
        $closureVar: 30px; // Local variable

        font-size: $closureVar + $enclosingVar + $globalVar; // 60px
    }
}

这也意味着,在SCSS中,变量的作用域完全依赖于代码的结构。然而,CSS自定义属性像其他CSS属性一样,具有继承的特性。

自定义属性不能有一个全局变量在选择器之外声明 —— 这不是有效的CSS。CSS自定义属性的全局作用域其实就是:root作用域,于是:root声明的自定义属性就是全局变量。

让我们用熟悉的SCSS语法知识用于HTML和CSS。我们将创建演示CSS自定义属性的示例。首选来看HTML部分:

<!-- Global -->
<div class="enclosing">
    Enclosing
    <div class="closure">
        closure
    </div>
</div>

CSS样式如下:

:root {
    --globalVar: 10px;
}
.enclosing {
    --enclosingVar: 20px;
}

.enclosing .closure {
    --closureVar: 30px;
    font-size: calc(var(--closureVar) + var(--enclosingVar) + var(--globalVar)); 
}

到目前为止,我们还没有看到这和SCSS变量有何不同之处。让我们重新分配变量后的用法。先来看SCSS的情况,但没有效果:

.closure {
    $closureVar: 10px; // Local Variable
    font-size: $closureVar + $enclosingVar + $globalVar;
    $closureVar: 50px; // Local Varialbe
}

但在CSS,计算的值因为改变了--closureVar的值重新计算了值,因此改变了font-size的值:

.enclosing .closure {
    --closureVar: 30px;
    font-size: calc(var(--closureVar) + var(--enclosingVar) + var(--globalVar);
    --closureVar: 50px;
}

这是一个较大的差异:如果你重新分配一个自定义属性的值,浏览器将重新计算calc()表达式所运用的变量

CSS处理器不知道DOM结构

假设我们想要在块元素中除了带有highlighted类的元素中使用默认的font-size:

<!-- HTML -->
<div class="default"> 
    default 
</div> 

<div class="default highlighted"> default highlighted </div>

我们使用CSS自定义属性:

.highlighted {
    --highlighted-size: 30px;
}

.default {
    --default-size: 10px;

    font-size: var(--highlighted-size, var(--default-size))
}

因为第二个HTML无素除了default类名还使用了highlighted类名,所以highlighted属性将被应用到这个元素。在这个示例中,它的意思是--highlighted-size: 30px将会被运用,进而将--highlighted-size将用于font-size上。

现在我们在CSS处理器(比如SCSS)中实现同样的事情:

// SCSS
.highlighted {
    $highlighted-size: 30px;
}

.default {
    $default-size: 10px;

    @if variable-exists(highlighted-size) {
        font-size: $highlighted-size;
    }

    @else {
        font-size: $default-size;
    }
}

事实上,$default-size都用于这两个元素,$highlighted-size并无生效。这是因为SCSS的计算和处理都发生在编译的时候,当然,它不知道任何关于DOM结构,完全依赖于代码的结构。CSS自定义属性却不同,其具有封必的作用域和级联的CSS属性和DOM结构有关系。

简单地说,CSS自定义属性可以理解DOM的结构和动态改变

基本运算和逻辑运算

CSS处理器具备基本运算和逻辑运算等特性,其可以类似于其他程序语言一样,比如在SCSS中,可以直接进行四则运算(+-*/%)、比较运算(><>=<=)、相等操作(==!=)、逻辑运算(andornot)以及条件判断@if@else和遍历@each@for 等操作。但在CSS自定义属性中这方面显得弱小一些,只能借助calc()函数做一些基本的运算操作。

.card {
    --gap: 10;
    padding: calc(var(--gap) * 1px) 0;
}

在CSS处理器中的@if@else等特性可以帮助我们在代码中做一些条件判断的操作。但在CSS自定义属性中是没有@if@else这样的特性。不过,我们可以借助CSS自定义属性的相关特性配合calc()函数来实现一个类似于if ... else这样的条件判断功能。假设有一个自定义属性--i,当:

  • --i的值为1时,表示真(即打开)
  • --i的值为0时,表示假(即关闭)

来看一个小示例,我们有一个容器.box,希望根据自定义属性--i的取值为01做条件判断:

  • --i的值为1时,表示真,容器.box旋转30deg
  • --i的值为0时,表示假,容器.box不旋转

代码可能像下面这样:

:root {
    --i: 0;
}

.box {
    // 当 --i = 0 » calc(var(--i) * 30deg) = calc(0 * 30deg) = 0deg
    // 当 --i = 1 » calc(var(--i) * 30deg) = calc(1 * 30deg) = 30deg
    transform: rotate(calc(1 - var(--i)) * 30deg))
}

.box.rotate {
    --i: 1;
}

或者

:root { 
    --i: 1; 
} 

.box { 
    // 当 --i = 0 » calc((1 - var(--i)) * 30deg) = calc((1 - 0) * 30deg) = calc(1 * 30deg) = 30deg 
    // 当 --i = 0 » calc((1 - var(--i)) * 30deg) = calc((1 - 1) * 30deg) = calc(0 * 30deg) = 0deg transform: rotate(calc((1 - var(--i)) * 30deg)) 
} 

.box.rotate { 
    --i: 0; 
}

整个效果如下图:

上面演示的是01之间的切换,其实还可以非零之间的切换,非零值之间的切换相对而言要更为复杂一些,这里就不做过多的阐述,如果感兴趣的话,可以阅读 @Ana 的两篇博文:

我把上面两篇文章整合到一起,可以阅读《如何通过CSS自定义属性给CSS属性切换提供开关》一文。

CSS处理器除了上述所说的特性之外,有些处理器(比如SCSS)还有一些函数功能,这些函数功能可以帮助CSS处理器更好的处理代码,提供更强大的特性。对于CSS自定义属性来说,这方面也可以借助CSS函数特性来做一些更强的事情。比如前面的示例中,我们有看到过color-mod()的身影:

.alert button {
    border-color: color-mod(var(--background-color), darken(25%));
}

CSS自定义属性的作用域

当自定义属性被var()函数引用的时候,该自定义属性又被称为变量。在我们熟悉的编程语言中,比如说JavaScript,变量是有作用域这样的概念存在的。那么CSS自定义属性也有作用域的概念。接下来我们就来聊聊CSS自定义属性作用域相关的内容。

CSS中第一个变量currentColor

在聊CSS自定义属性作用域之前,先来聊聊CSS中的另一个变量,即currentColor

CSS中最早的变量是currentColor,但不是属性,而是CSS的属性值。虽然currentColor被称为变量,但它的使用有一个限制:

currentColor只可以在接受<color>值的地方使用;如果CSS的属性不接受<color>值,就不能接受currentColor作为值

在CSS中能接受<color>值的属性比较多,比如border-colorbackground-colorbox-shadowtext-shadowoutline以及CSS渐变属性等。如果我们希望元素的边框、阴影、背景等颜色和color同步,那么我们就可以使用currentColor

currentColor的值是由当前元素使用的color属性的计算值决定的

currentColor的帮助下,可以让我们更好的扩展颜色级联。也就是说,当color值改变的时候,使用currentColor的颜色属性也会随之改变。比如:

虽然currentColor和CSS自定义属性都称为变量,但两者之间还是有一定的差异。我将通过一个简单的案例来向大家阐述两者的差异。假设我们有一个这样的场景,希望标题的边框和正文的颜色应用同一种颜色,如下图所示:

事实上实现上图的效果并不是什么难事。比较难的是我们如何以最少的代码、最简易方式来实现,而且还具备换肤的需求。假设我们实现上图效果的DOM结构是这样的:

<!-- HTML -->
<body>
    <h3>我是一个标题</h3>
    <p>我是一段文本,文本的颜色和标题边框色是一样的...</p>
</body>

在实现上图效果之前,给自己加码(加上一些实现的限制条件):

只想在一个地方设置颜色值,而h3border-color要继承这个颜色。

目的是希望标题的边框颜色和主体文本颜色一样。这个时候,你可能会想到CSS级联中的继承关键词inherit

body {
    color: red;
}

h3 {
    color: #000;
    border: 3px double;
    border-color: inherit;
}

但最终的效果和你预期的并不一样:

为什么会这样呢?简单地和大家一起分析一下。

虽然在body中显式的设置了color的值为red,但并未显式设置border-color的值。CSS的规则就是如果未显式给CSS属性设置值话,就会采用其初始值(默认值)。而border-color的默认值是currentColor。也就是说,基于此例的上下文,currentColor的值是red,这也是预期想要继承的值。

既然如此,inherit下来的值不是red呢?这主要是因为我们在CSS中,显式的给h3设置了color的值为#000,而border-color从父元素中继承过来的值是currentColorcurrentColor的值又和color的值有着紧密的联系,如此一来,最终渲染出来的边框色就是#000。这样一来,要解决这个问题就显得非常的简单,只需要在body元素上显式设置border-color的值为red即可:

body {
    color: red;
    border-color: red;
}

h3 {
    color: #000;
    border: 3px double;
    border-color: inherit;
}

现在h3继承过来的值不再是变量currentColor,而是继承了bodyborder-color,即red。从而得到我们想要的效果:

使用currentColor实现我们想要的一个效果,但CSS自定义属性的出现,使用CSS自定义属性来处理这个效果会显得更容易,维护性也更好:

body {
    --color: red;
    color: var(--color);
    border-color: var(--color);
}

h3 {
    --color: #000;
    border: 3px double;
    border-color: inherit;
}

在这种情况之下,h3border-color继承了父元素body的自定义属性--color。但是,即使h3color的值是本地的自定义属性--color(局部变量),它的border-color也不会像currentColor那样使用这个本地声明的自定义属性值。

继承自定义属性设置的值始终与父属性解析的值相匹配。本例中的color属性将取局部的值,因为它不是继承的。

currentColor和CSS自定义属性的关键区别是:

currentColor关键词不是在计算值时解析,而是对本地color属性的使用值的引用

CSS中的作用域

在聊currentColor这个CSS变量时,我们提到局部变量这个词。但在CSS中对于作用域的概念来说却又很模糊,为了大家更好的理解CSS自定义属性的作用域,我们有必要花一点时间来理解CSS中的“作用域”。换句话说,如何更好的理解CSS作用域?(这是一个令很多人头痛的问题)。

@ppk他的博客中就曾讨论过CSS作用域。在CSS中任何选择器都是全局的(在整个文档中都是有效的)。比如说:

p {
    color: red;
}

上面的代码会让整个文档中所有p元素的color值为red。但很多时候,总是需要为不同地方的p元素设置不一样的color值,这个时候只能通过在p元素前面添加别的元素、类名或ID,比如:

/* 文档所有p元素 */
p {
    color: red;
}

/* 文档所有div中的p元素 */
div p {
    color: blue;
}

/* 文档所有.blog中的p元素 */
.blog p {
    color: green;
}

/* id为blog中的p元素 */
#blog p {
    color: orange;
}

这和文档源码结构有着紧密的关系,如果文档结构发生变化,那么你的选择器范围也将随着发生改变。

另外,就算是你在p元素前面添加了模块(范围),除了受限于其文档结构的约束之外,还受限于CSS选择器权重的约束。拿下面这个例子来说:

<!-- HTML -->
<div id="outer">
    <p>我是什么颜色?</p>
    <div id="inner">
        <p>我是什么颜色?</p>
    </div>
</div>

// CSS
p {
    color: red;
}

#inner p {
    color: green;
}

#outer p {
    color: orange;
}

虽然在p元素前面添加了范围限制#inner p#outer p,但根据CSS的选择器权重和级联方面的特性,可以知道,#inner p#outer p选择器权重是一样的,这个时候哪个选择器在后面,谁被运用的机会就更高(相同权重是,后面的会覆盖前面的)。因此,最终示例中的p元素的文本颜色都会是orange。如果你将两者顺序更换一下:

#outer p {
    color: orange;
}

#inner p {
    color: green;
}

这个时候,上例中的p元素的文本颜色都会是green。这就是令人头痛的CSS选择器问题,也是所谓的作用域问题。

值得庆幸的是,随着一些JavaScript框架的出现,社区中已有许多CSS-in-JS的方案,其主要目的之一就是使用合理的、可读的语法来限定CSS选择器的范围

然而,CSS作用域名的问题似乎在不远的装饰来就会得到解决。换句话说:

在CSS中有限的局部范围(选择器局部作用域)已经存在。

CSS自定义属性的作用域

CSS自定义属性(也称CSS变量)其实是已经有了作用域。可以为特定元素及其后代重新定义自定义属性的值

在深入聊CSS自定义属性作用域之前,我们先来回忆JavaScript和CSS处理器(比如SCSS)中的作用域。众所周知,在JavaScript中的变量var,它的作用范围跟function有关,而letconst声明的变量作用范围和块作用域有关。

来看JavaScript闭包的示例:

// JavaScript
window.globalVal = 10;

function enclosing() {
    var enclosingVar = 20

    function closure() {
        var closureVar = 30

        return globalVal + enclosingVal + closureVar
    }

    return closure()
}

closure()访问外部(封闭)函数作用域的变量。闭包有三个范围,它的访问方式如下:

  • 自已的作用域范围({}内的变量)
  • 外部函数的变量
  • 全局变量

在SCSS中和JavaScript有点类似。在任何选择器或构造器(比如混合宏@mixin)声明的变量是全局变量,否则是局部变量。而且任何嵌套的代码块都可以访问封闭内的变量:

$globalVar: 10px; // Global variable

.enclosing {
    $enclosingVar: 20px; // Local variable

    .closure {
        $closureVar: 30px; // Local variable

        font-size: $closureVar + $enclosingVar + $globalVar
    }
}

在前面的示例中向大家演示了使用CSS自定义属性的两种方式:

:root {
    --color: red;
}

p {
    color: var(--color);
}

#inner p {
    --color: green;
    color: var(--color); 
}

在显式声明CSS自定义属性的时候,常见的方式如上图所示,一种是在:root{}选择器块内,另一种是在非:root{}选择器内(比如上图中的#inner p {}选择器)。其中在:root选择器内声明的CSS自定义属性作用于全局范围(其作用域称为全局),而非:root选择器内声明的CSS自定义属性作用于该选择器区块内以及其后代元素内(其作用域称为局部或本地)。

很多时候大家都会把:roothtml等同起来,事实上并非如此,因为html的权重大于:root,好比带有类名的div权重大于元素标签div,比如:

:root {
    --color: red;
}

html {
    --color: green;
}

.class {
    --color: orange;
}

这样一来,其作用域是:

  • .class中的--color会作用于类名为.class元素以及其所有后代元素
  • html中的--color会作用于<html>元素以及其所有后代元素
  • :root中的--color在HTML中会作用于<html>以及其所有后代元素,在XML中(比如svg)则会作用于<svg>以及其所有代元素

因此,如果你要声明一个全局作用域的CSS的自定义属性,最佳方式是在:root选择器内声明。这也是为什么大家更喜欢在:root中声明自定义属性,而不太会选择在html元素上声明全局的CSS自定义属性。除此之外,在:root中声明CSS自定义属性还有助于将以后要使用的CSS自定义属性与正在用于文档的样式化的选择器分离开来。

既然如此,那么是不是所有自定义属性都应该在:root中声明呢?在回答这个问题之前,我们回过头来看SCSS中变量的声明方式。在SCSS中,在所有块外(任何选择器之外声明的变量)声明的变量为全局变量,可以运用于任何地方。这很有用,因为这样做可以很清楚的自己要做的事情。

如果按同样的思维方式来考虑的话,是不是所有变量都在:root中声明,比如:

:root {
    --clr-light: #ededed;
    --clr-dark: #333;
    --clr-accent: #EFF;
    --ff-heading: 'Roboto', sans-serif;
    --ff-body: 'Merriweather', serif;
    --fw-heading: 700;
    --fw-body: 300;
    --fs-h1: 5rem;
    --fs-h2: 3.25rem;
    --fs-h3: 2.75rem;
    --fs-h4: 1.75rem;
    --fs-body: 1.125rem;
    --line-height: 1.55;
    --font-color: var(--clr-light);

    /* 可能只用于 navbar */
    --navbar-bg-color: var(--clr-dark);
    --navbar-logo-color: var(--clr-accent);
    --navbar-border: thin var(--clr-accent) solid;
    --navbar-font-size: .8rem;

    /* 可能只用于header */
    --header-color: var(--clr-accent);
    --header-shadow: 2px 3px 4px rgba(200,200,0,.25);

    --pullquote-border: 5px solid var(--clr-light);
    --link-fg: var(--clr-dark);
    --link-bg: var(--clr-light);
    --link-fg-hover: var(--clr-dark);
    --link-bg-hover: var(--clr-accent);
    --transition: 250ms ease-out;
    --shadow: 2px 5px 20px rgba(0, 0, 0, .2);
    --gradient: linear-gradient(60deg, red, green, blue, yellow);

    /* 可能只用于button */
    --button-small: .75rem;
    --button-default: 1rem;
    --button-large: 1.5rem;
}

上面这种方式,把所有CSS自定义属性在:root中声明,便于我们管理CSS自定义属性,也知道他们用于什么地方。但是,为什么我们要在:root中声明有关于navbarheaderbutton相关的CSS自定义属性。按照我的理解来说,他们应该是局部的,并不会用于全局。既然它们不是全局属性,为什么要在:root中显式的声明呢?这就是CSS自定义属性局部作用域该发挥作用的地方了。

如果你要看示例的话,就可以回过头看“CSS自定义属性在Web组件中的应用”一节。我们在设计Button组件的时候,就很好的利用了CSS自定义属性作用域的概念和设计理念。

我们在使用CSS自定义属性的时候,可通过大小写来区分全局和局部作用域的CSS自定义属性。因为CSS自定义属性是区分大小写的,所以更建议全局作用域的CSS自定义属性用大写,而局部作用域名的CSS自定义属性使用小写。比如:

:root {
    --PRIMARY: #f36;
}

.button {
    --button-primary: var(--PRIMARY);
}

CSS自定义属性允当变量被var()函数使用的时候,还可以提供降级值。那么在实际使用的时候,我们应该避免直接覆盖全局作用域的CSS自定义属性,这样做的目的是心量避免全局作用域的CSS自定义属性和他的值分离。为了达到这种需求,就可以采用降级值来实现这一点。比如:

:root {
    --THEME-COLOR: var(--user-theme-color, #f36);
}

body {
    --user-theme-color: green;
    background-color: var(--THEME-COLOR);
}

像这样间接设置全局动态属性可以防止它们在局部被覆盖,并确保用户的设置始终都从根元素继承。这个约定能避免主题参数被意外的继承。

CSS自定义属性运用场景

时至今日,CSS已得到主流浏览器的支持。而且CSS自定义属性可以很好的帮助我们改变思考和构建CSS的方式。在很多场景上我们都可以使用CSS自定义属性。使用CSS自定义属性可以除了其灵活性之外,还可以让我更好的维护自己的CSS代码。

举几个使用CSS自定义属性的例子。

使用CSS自定义属性改变颜色

CSS的颜色模式有很多种,常见的rgbrgbahslhsla等。很多时候我们借助JavaScript来操作CSS颜色,CSS自定义属性出现之后,我们也可以借助CSS自定义属性来修改颜色:

:root {
    --COLOR-R;
    --COLOR-G;
    --COLOR-B;
    --COLOR-A;
    --THEME-COLOR: rgba(var(--COLOR-R), var(--COLOR-G), var(--COLOR-B), var(--COLOR-A));
}

CSS自定义属性换肤

很多时候在Web开发的时候,都有可能会有换肤这样的需求。借助CSS自定义属性可以让我们换肤变得更容易。比如说我们想要实现一个高亮模式和黑暗模式之间的切换

使用CSS自定义属性还可以实现更为复杂的主题切换的效果:

使用CSS自定义属性控制流式排版

以前我们在多篇文章中介绍了精准的流体布局的方法:

一般我们都是利用视窗单位vwcalc()函数来计算font-size或者padding之类:

如果我们使用CSS自定义属性的话,可以将上面公式中的值换成CSS自定义属性,从而得到流式排版的效果:

CSS自定义属性在Web组件中的应用

在《CSS 自定义属性在Web组件中的应用》一文中曾和大家深入的探讨过CSS自定义属性为Web组件设计带来什么样的便利性。简单地说,怎么使用CSS自定义属性更好的为Web设计提供类似API接口,让使用者能更好的使用Web组件,以及更灵活的扩展和覆盖组件的UI风格:

CSS自定义属性在Web Animation中的运用

使用CSS自定义属性可以很好的和Web Animation结合起来:

也可以灵活的和SVG动效结合在一起,实现一些微动效的效果:

还可以结合鼠标或者手势实现响应式的动效:

其他Demo

Codepen上有很多关于CSS自定义属性的Demo。感兴趣的话可以去上面查阅。

小结

在这篇文章中,我们花了很长的篇幅和大家一起探讨了CSS自定义属性相关的知识。到目前为止,主流浏览器对CSS自定义属性已经有了较好的支持,我们可以大胆的开始使用她。如果你担心兼容性问题的话,可以考虑借助PostCSS相关的插件(postcss-custom-properties)来对CSS自定义属性做降级处理。

这篇文章有关于CSS自定义属性相关的介绍,不是最全面的,但涉及的内容是最多的。希望对你有所帮助。如果你在这方面有较好的建议或相关的经验,欢迎在下面评论中与我们一起分享。

另外,在CSS Houdini中也有CSS自定义属性相关的内容,在下一节中,将和大家一起探讨CSS Houdini中CSS自定义属性相关的内容。感兴趣的同学,欢迎持续关注相关的更新。

返回顶部