CSS自定义属性你知多少

发布于 大漠

很多Web开发者更喜欢将 *CSS自定义属性 称之为 CSS变量。在2020年中社区中有关于CSS自定义属性的讨论到处可见,小站也有很多关于CSS自定义属性的相关教程,而且在很多Web应用中也可以看到CSS自定义属性的身影。虽然如此,但有很多开发者对CSS自定义属性了解的不多,甚至说不怎么理解,也用不好CSS自定义属性。这篇文章和以往介绍CSS自定义属性的文章有所不同,我从不同的角度来阐述CSS自定义属性,主要是希望这篇文章能让Web开发者更好地理解CSS自定义属性,以及如何更好的使用该属性。如果你对该话题感兴趣的话,请继续往下阅读。

CSS自定义属性的发展进程

众所周之,CSS和其他程序语言有一个最大的差异,即 CSS没有变量 这样的概念(也没有逻辑),也正因此,很多Web开发者都觉得CSS是非常简单的,没有技术含量的。

然而,在前端开发者,很多时候希望CSS能像其他程序语言一样,有 变量 的概念,这样利于CSS的编写和维护。比如构建像下图这样的UI Kit:

从上图可以发现,很多UI上都有#0055fe这个色值,也就是说该色值会在CSS样式表中使用多次:

header {
    background-color: #0055fe;
}

label {
    color: #0055fe;
}

button {
    border: 1px solid #0055fe;
}

想象一下,如果你正在维护一个大型的项目,会涉及很多个组件(或者多个.css样式文件),即 #0055fe被分散运用于多个文件中,多个样式块中。突然有一天,你被要求改更颜色(换肤)。那么在没有CSS自定义属性(CSS变量),可能最好的办法就是在整个项目的所有.css文件中查找#0055fe,然后再替换。这么做,是多么痛苦的一件事情,而且还容易被遗漏

这样的模式可以说是痛苦的,但庆幸的是,在CSS社区中开始使用像Sass、LESS和Stylus等CSS处理器,开发者可以在这些处理器中开始使用变量的概念,比如在Sass中:

$primary-color: #0055fe;

header {
    background-color: $primary-color;
}

label {
    color: $primary-color;
}

button {
    border: 1px solid $primary-color;
}

如此一来,我们可以在一个_var.scss中放置所有样式中会用到的变量,比如$primary-color,然后在需要的地方引用已定义好的变量,目的是 为了实现CSS的值的可重用性和减少冗余。基于该特性,Web开发者可以轻易的实现换肤效果:

另外,CSS这几年发展和变革是非常地快,而且W3C的CSS工作者也知道,CSS也应该具备“变量”这样的特性,为开发者减少重复性的工作和简化工作,并且减少对CSS处理器工具的依赖。为此,2012年左右,W3C CSS小组为CSS加入了 CSS自定义属性(CSS变量) 模块,并在2017年左右获得大部分主流浏览器的支持。

有了CSS自定义属性后,我们可以像下面这样来维护CSS:

:root {
    --primary-color: #0055fe
}

header {
    background-color: var(--primary-color);
}

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

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

不过CSS原生的自定义属性(变量),它也有一定的缺陷,比如说无法在声明变量的时候指定其语法类型,比如上面示例中,我们只能在:root{}中指定--primary-color自定义属性的值是#0055fe,它只是个字符串,并不是一个<color>

除了原生的CSS中可以声明自定义属性之外,CSS Houdini属性和值API 对CSS自定义属性进行了扩展:

对于CSS Houdini中的自定义属性,我更喜欢称之为 CSS Houdini变量。CSS Hounini变量有两种方式来注册,一种是JavaScript来注册

CSS.registerProperty({ 
    name: '--primary-color',
    syntax: '<color>', 
    inherits: false, 
    initialValue: '#0055fe' 
})

另外一种是使用@property注册

@property --primary-color { 
    syntax: '<color>'; 
    initial-value: #0055fe; 
    inherits: false; 
}

CSS Houdini变量的使用方式和CSS原生的CSS变量使用方式是相同的:

header {
    background-color: var(--primary-color);
}

时至今日,你可能还在CSS处理器中使用变量,或许在开始使用原生的CSS变量,也有可能两者混合在一起使用。换句话说,我们有多种方式使用CSS变量,但我们应该根据具体的场景使用更合适合的方式,不过我自己更建议从现在开始就使用原生的CSS变量,因为它有些特性是CSS处理器中变量无法具备的,特别是使用CSS Houdini的变量时,它的特性会变得更强大。在接下来的内容中,有可能会用到CSS Houdini的变量。

CSS自定义属性的基础

上面我们主要介绍了CSS自定义属性(或变量)的发展与变迁,可能对CSS自定义属性了解的还不够深入。如果你从未接触过该方面的特性,可以从CSS自定义属性的一些基础开始,如果你是这方面的专家,你可以选择性的阅读后续的内容。

那我们从CSS自定义属性最基础的开始吧!

CSS自定义属性简述

CSS自定义属性也常被称为CSS变量,被称为CSS变量主要还是源于CSS处理器或其他程序语言的一种叫法。但我想说的是“CSS自定义属性不是变量”。为什么这么说呢?后面会向大家解释。

CSS自定义属性是以--前缀开始命名,比如前面示例中的--primary-color,其中primary-color可以是任何字符串,它也被称为“变量名”。即--变量名(比如--primary-color)组合在一起才是“CSS自定义属性”。

CSS自定义属性的声明和Sass的变量声明有所不同,在Sass中,我们可以在非{}外声明,比如:

$primary-color: #0055fe;

但CSS自定义属性声明需要放置在一个{}花括号内,比如:

:root {
    --primary-color: #0055fe;
}

除了在:root中之外,还可以是在其他的代码块中,比如:

html {
    --primary-color: #0055fe;
}

header {
    --primary-color: #00fe55;
}

虽然按上面的方式在CSS中注册了CSS自定义属性,但如果没有被var()函数引用的话,它们不会有任何效果。比如下面这个示例,只有--primary-colorvar()引用,而--gap虽已注册,但未被var()引用,它也就未运用到任何元素上:

:root {
    --primary-color: #0055fe;
    --gap: 20px; 
}

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

除了在CSS中使用--varName来注册一个CSS自定义属性之外,我们还可以使用JavaScript的style.setProperty()动态注册一个CSS自定义属性,比如:

document.documentElement.style.setProperty('--primary-color', '#0055fe')

执行完之后,在<html>元素上会添加style属性:

<html style="--primary-color: #0055fe"></html>

CSS Houdini中,我们还可以使用另外两种方式来注册CSS自定义属性(变量)。在CSS样式文件中可以使用@property注册自定义属性

@property --primary-color {
    syntax: '<color>'; 
    initial-value: #0055fe; 
    inherits: false; 
}

JavaScript中可以使用CSS.registerProperty()注册

CSS.registerProperty({ 
    name: '--primary-color',
    syntax: '<color>', 
    inherits: false, 
    initialValue: '#0055fe' 
})

CSS Houdini中注册好的CSS自定义属性同样只有被var()函数调用才能生效。

有一点开发者需要特别注意,CSS中注册的自定义属性是有大小写之分的,比如--on--ON是两个不同的CSS自定义属性,比如:

:root {
    --ON: 1;
}

.box {
    transform: rotate(calc(var(--ON) * 45deg));
    transition: transform 1s ease-in-out;
}

.box:hover {
    transform: rotate(calc(var(--on) * 720deg));
}

.box:last-of-type:hover{
    transform: rotate(calc(var(--ON) * 720deg));
}

如果你把鼠标移动蓝色.box上,效果和我们预想的并不相同,没有旋转720deg,反而旋转到了0deg,即--on无效值;如果把鼠标移动到红色的.box上,可以看到元素从45deg旋转到720deg

从浏览器开发者工具中,我们可以得到,var(--on)(注意,我们在代码中并没有显式声明--on这个自定义属性),那么transform: rotate(calc(var(--on) * 720deg))计算出来的transfromnone

那这就引出了第一个问题:当一个var()函数使用一个未定义的变量时,会发生什么

当一个var()函数使用一个未定义的变量时,会发生什么?

上面的示例告诉我们:var()函数使用一个未定义的变量(自定义属性)并不会导致样式解析错误,也不会阻止样式加载、解析或渲染。这个就好比你在编写CSS时,因为手误将属性或属性值用错一样,客户端只是不识别这个错误的信息,比如:

那么在使用var()中使用一些未定义的CSS变量时,有可能是:

  • var()函数引用的变量名输错了(手误造成)
  • 你可能使用var()引用了一个自认为它存在的CSS变量,但事实上它并不存在
  • 你可能正试图使用一个完全有效的CSS变量,但是你想在其他地方使用,它恰好不可见

我们再来看两个示例,先来看一个有关于border的示例:

:root {
    --primary-color: #0055fe;
}

body {
    color: #f36;
}

.box {
    border: 5px solid var(--primay-color);
    color: var(--primay-color);
}

你将看到的效果如下:

由于手误,在bordercolorvar()函数事实上引用了一个并未定义的CSS变量--primay-color(其实是想引用--primary-color)。结果浏览器并不知道border最终的值应该是什么?因为border属性在CSS中是一个不可继承的属性,这个时候浏览器会理解成用户把border属性值写错了。此时,border会被浏览器解析为border: medium none currentColor

注意,在CSS中,如果border-style的值被渲染为none时,你是看不到任何边框效果的。

再来看color属性。虽然var()引用的变量也手误写错了,但它却有颜色。这主要是因为color是一个可继承的属性,所以浏览器渲染的时候会继承其祖先元素的color值,在我们这个示例中,在body中显式设置了color: #f36,因此.boxcolor继承了bodycolor值,即#f36

这个示例令人感到困惑的是var()引用错语的变量(其实是不存在的变量),浏览器渲染的时候到底会发生什么?从上面的示例中我们可以得知,它的根源在于var()函数使用了无效的属性,这个时候浏览器渲染CSS时,它自己也无从得知。

浏览器在渲染CSS时,只有属性名或值无法被识别时(浏览器渲染引擎不知道时)才会认为是无效的。但是,var()函数可以解析为任何东西,所以样式引擎不知道var()包含的值是否已知(浏览器渲染引擎可识别)。只有当这个属性真正被使用时,它才会知道,这时,它会默默地回退到属性的继承或初始状态,并让你疑惑发生了什么?当你碰到这个现象的时候,其实可以借助浏览器工发者工具来查找问题:

除此之外,在一些浏览器中还提供了自定义属性和其值的查找,有关于这个部分,我们在介绍浏览器开发者工具的时候会向大家演示。

不知道你有没有发现,CSS原生中的自定义属性是一个字符串(前面有提到过),可以说并不很严谨。比如说,--primary-color应该是一个颜色值(<color>),但有的时候在另外一个地方再次注册的时候,它可能被开发者定义成一个长度值(<length>),比如:

:root {
    --primary-color: #0055fe;
}

.box {
    --primary-color: 5px;
    border: solid var(--primary-color);
    color: var(--primary-color);
}

效果如下:

可以看到,.box中的border引用自己作用域中注册的--primary-color,浏览器这个时候将其解析为border-width: 5px,而color也同时引用了--primary-color,可相当于color: 5px,此时浏览器将其继承祖先元素<body>color值。

如果你尝试着将.box{}中的--primary-color禁用,此时bordercolor中的--primary-color将会引用全局的(即:root{})中注册的值(--primary-color: #0055fe)。此时border中的var(--primary-color)被浏览器解析为border-color: #0055fe,而border-width被解析为medium。而color中的var(--primary-color)也就是一个有效值了。

感觉是不是有点混乱呢?其实这里有一个关于CSS自定义属性(变量)的作用域概念。稍后再探讨。

CSS中的自定义属性的值类型没有任何约束,也就造成上面示例中提到的效果。如果你想对自定义属性值的类型有较强约束的话,就可以使用CSS Houdini的变量了,因为它有syntax属性来指定自定义属性的值类型。比如:

@property --primary-color {
    syntax: '<color>'; 
    initial-value: #0055fe; 
    inherits: false; 
}

这样做除了指定了--primary-color值类型之外,还可以在var()直接引用--primary-color(会解析成初始值,即initial-value指定的值)。如果你在var()中引入的--primary-color自定义属性的值不是<color>类型,浏览器引用其初始值,比如:

@property --primary-color {
    syntax: '<color>'; 
    initial-value: #0055fe; 
    inherits: false; 
}

/* 使用 --primary-color初始值 */
.initial__value {
    border: 5px solid var(--primary-color);
}

/* 重置 --primary-color值 */
.new__value {
    --primary-color: #09f;
    border: 5px solid var(--primary-color);
}

/* 无效值,但会引用--primary-color初始值 */
.invalid__value {
    --primary-color: 5px;
    border: solid var(--primary-color);
    color: var(--primary-color);
}

效果如下:

var()函数还可以提供回调值

var()引用了一个未定义的自定义属性时,可能会给开发者带来一定的困惑。除了上一节提到的相关知识之外,我们还可以使用var()函数的第二个参数来作为其回调值。

var() = var( <custom-property-name> [, <declaration-value> ]? )

这种方式对于前面提到的现象,比如var()函数引用了未定义(或手误写错了自定义属性名称),就有一个降级处理。也就是说,如果var()函数使用了第二个参数时,第一个参数引用错误的时候会使用第二个参数。比如下面这个示例:

:root {
    --primary-color: #0055fe;
}

.box {
    background-color: var(--primy-color, #f36);
}

从代码中可以看得出来,var()其实引用了一个没有定义的自定义属性(我们定义的是--primary-color,实际引用的是--primy-color),这个时候var()引用了第二个参数#f36,因此.box的背景色是#f36

不仅如此,我们还可以在var()中的第二个参数也可以使用var()

.box {
    background-color: var(--primary-color, var(--black, #f36));
}

甚至还可以更深层的嵌套,比如:

color: var(--foo, var(--bar, var(--baz, var(--are, var(--you, var(--crazy)))));

虽然var()具备这方面的特性,但在实际使用的时候并不推荐这样使用,因为层级嵌套的越深越容易出错,而且这样同时增加了代码维护的成本。

在CSS Houdini中注册CSS变量(自定义属性)时,如果将inherits参数设置为一个布尔值,CSS渲染引擎将发送初始值(initial-value)作为它的回退值(回调值)。比如下面这个示例:

@property --colorPrimary {
    syntax: '<color>';
    initial-value: #0055fe;
    inherits: false;
}

.card {
    background-color: var(--colorPrimary); /* #0055fe */
}

.card__highlight {
    --colorPrimary: #09f;
    background-color: var(--colorPrimary); /* #09f */
}

.card__another {
    --colorPrimary: 23;
    background-color: var(--colorPrimary); /* #0055fe */
}

--colorPrimary的初始值是#0055fe,但在.card__another中开发者使用了一个无效的值23。如果没有@property,CSS解析器将忽略这个无效值。现在,解析器取的是#0055fe初始值,这也就是说,可以在CSS中使用真正的回退值。

我们换过一个示例,先来看示例的HTML:

<!-- HTML -->
<section>
    <h1>CSS Houdini</h1>
    <div></div>
</section>

同样使用@property来注册自定义属性--colorPrimary,并且inherits的值为true

@property --colorPrimary {
    syntax: "<color>";
    initial-value: magenta;
    inherits: true;
}

section {
    --colorPrimary: #09f;
    border: 5px solid var(--colorPrimary);
}

/* 继承了section中声明的--colorPrimary */
h1 {
    color: var(--colorPrimary, #f36)
}

div {
    --colorPrimary: #23; /* 这是一个无效值 */
    background-color: var(--colorPrimary); /* 继承了section中声明的--colorPrimary */
}

如果把inherits的值设置为false

@property --colorPrimary {
    syntax: "<color>";
    initial-value: magenta;
    inherits: false;
}

section {
    --colorPrimary: #09f;
    border: 5px solid var(--colorPrimary);
}

h1 {
    color: var(--colorPrimary, #f36); /* magenta */
}

div {
    --colorPrimary: #23; /* 这是一个无效值 */
    background-color: var(--colorPrimary); /* magenta */
}

效果对比如下:

CSS自定义属性的作用域

众所周之,CSS令人感到头痛之一就是不像其他程序语言,没有作用域名的概念。不过,CSS变量(CSS自定义属性)的出现打破了这种局限性,因为CSS变量可以像其他程序语言一样可以有作用域的概念。以JavaScript为例,在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中的自定义属性命名(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自定义属性的作用域概念:

/* 全局变量 */
:root {
    --primary-color: #235ad1;
    --unit: 1rem;
}

/* 运用了全局变量 */
.section-title {
    color: var(--primary-color);
    margin-bottom: var(--unit);
}

/* 自定义了局部变量,将覆盖全局变量 */
.featured-authors .section-title {
    --primary-color: #d16823;
}

/* 自定义了局部变量,将覆盖全局变量  */
.latest-articles .section-title {
    --primary-color: #d12374;
    --unit: 2rem;
}

我们再回过头来看CSS Houdini中的变量,就拿@property注册变量为例吧。可以使用@property来注册一个CSS变量,比如:

@property --primary-color {
    syntax: '<color>'; 
    initial-value: #0055fe; 
    inherits: false; 
}

.box {
    border: 5px solid var(--primary-color);
}

.box__locatal {
    --primary-color: #f36;
}

从上面的示例中可以看得出来,@property注册的变量就是一个全局变量,只有在相应的选择器块中重置了@property注册的CSS变量(比如上面示例中的--primary-color)才是局部变量。

循环依赖的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

这也对应了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 Houdini中使用@property注册自定义属性时,如果同时注册两个相同名称的自定义属性,那么后面的将会覆盖前面的:

@property --colorPrimary {
    syntax: "<color>";
    initial-value: magenta;
    inherits: true;
}

@property --colorPrimary {
    syntax: "<color>";
    initial-value: #09f;
    inherits: false;
}

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

div {
    background-color: var(--colorPrimary)
}

如果使用CSS.registerProperty()同时注册相同的自定义属性时则会报错:

CSS.registerProperty({
    name: "--stop",
    syntax: "<length>",
    initialValue: "100px",
    inherits: false
});

CSS.registerProperty({
    name: "--stop",
    syntax: "<percentage>",
    initialValue: "50%",
    inherits: false
});

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

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

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

CSS自定义属性有助于行为和样式的真正分离

CSS和JavaScript同为Web的基石,其中CSS用来设计样式,JavaScript来实现Web的交互行为。而CSS自定义属性的到来,更有助于行为和样式的真正分离。为了更易于大家理解,我们通过一个简单的示例来阐述,比如我们有一个径向渐变(radial-gradient)或圆锥渐变(conic-gradient,让渐变的中心点能跟着鼠标移动。在过去,我们需要在JavaScript中创建整个渐变,并在每次鼠标移动时创建渐变。而有了CSS自定义属性,JavaScript只需要设置两个CSS自定义属性--mouse-x--mouse-y

/* CSS */
:root {
    --colors: red, yellow, lime, aqua, blue, magenta, red;
    --mouse-x: 50%;
    --mouse-y: 50%;
}

body {
    width: 100vw;
    height: 100vh;
    background-image: conic-gradient(
        at var(--mouse-x) var(--mouse-y),
        var(--colors)
    );
}

// JavaScript

const root = document.documentElement;

document.addEventListener('mousemove', evt => {
    let x = evt.clientX / innerWidth * 100
    let y = evt.clientY / innerHeight * 100

    root.style.setProperty('--mouse-x', `${x}%`)
    root.style.setProperty('--mouse-y', `${y}%`)
})

你可以尝试着在下面的Demo中移动鼠标时可以看到渐变的中心会随着鼠标移动:

我们还可以将径向渐变和锥形渐变结合起来,达到不同的效果:

:root {
    --colors: red, yellow, lime, aqua, blue, magenta, red;
    --mouse-x: 50%;
    --mouse-y: 50%;
}

body {
    width: 100vw;
    height: 100vh;
    --center: var(--mouse-x) var(--mouse-y);
    background-image: radial-gradient(
        circle at var(--center),
        red,
        blue 2%,
        transparent 2%
        ),
        conic-gradient(at var(--center), var(--colors));
}

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自定义属性模拟逻辑操作更详细的介绍,我将在后面会以单独的一节和大家探讨。

CSS自定义属性是响应式的

CSS自定义属性和CSS处理器变量最大的一个差异就是CSS自定义属性是响应式(Reactive)。它们在页面的整个生命周期内都是活的,更新它们会更新每个引用它们的属性的值。从这个意义上来说,CSS自定义属性更类似于Vue、Angular和React等框架中的属性,而不是传统编程或处理器程序中变量。因为它们是属性,所以它们可以通过任何更新CSS属性的机制来更新,比如样式表、内联样式,甚至是JavaScript。

CSS自定义属性不是变量

CSS自定义属性又常被称为 CSS变量,更多开发者更喜欢称CSS变量,这也可能是因为其他程序语言或CSS处理器中的变量带来的影响。而且很多博客或教程中,也常把“CSS自定义属性”和“CSS变量”两个词交替使用。

你可能也发现了,就在本文中,我也常把“CSS自定义属性”和“CSS变量”交替使用,也有在“CSS自定义属性”之后备注为“CSS变量”,或者在“CSS变量”之后备注“CSS自定义属性”。这一切都是习惯所致!

那为什么说“CSS自定义属性”不是“CSS变量”呢?我们先从相关的文档中来说起。

首先来看MDN文档,他是这样来描述的

Custom properties (sometimes referred to as CSS variables or cascading variables) … You can define multiple fallback values when the given variable is not yet defined.

大致意思是:

自定义属性(有时也被称为CSS变量级联变量)。当给定的变量尚未定义时,你可以定义多个回退值。

Google开发者网站(Web.dev)有篇文章对CSS Houdini的@property描述时提到

This API supercharges your CSS custom properties (also commonly referred to as CSS variables) … The --colorPrimary variable has an initial-value of magenta.

它的意思是:

你可以使用@propertyAPI来自定义CSS属性(也就是通常所说的CSS变量)。比如--colorPrimary变量的初始值是magenta

为什么这么混乱呢?这是有因可查的。其实在W3C的自定义属性描述的规范(CSS Custom Properties for Cascading Variables Module Level 1就出现了“CSS自定义属性”和“CSS变量”这两个术语。甚至在规范的标题也出现这两个术语“CSS Custom Properties for Cascading Variables”。

事实上呢?前面也提到过,CSS自定义属性也是CSS变量。不过,规范对这两个术语(“CSS自定义属性”和“CSS变量”)还是做了区分的:

CSS自定义属性不是一个CSS变量,但它定义了一个CSS变量

简单地说呢?在CSS代码块中使用--注册的属性称为“自定义属性”,但只有被var()引用的“CSS自定义属性”才能被称为“CSS变量”,而且其值由相关的自定义属性定义。

上图描述的是--accent-background自定义属性使用var(--main-color)(其中--main-color是CSS变量),其值由--main-color自定义属性定义

在CSS中,这样来区分“CSS自定义属性”和“CSS变量”是有用的,因为它允许我们讨论var()函数的回调值(CSS自定义属性像其他CSS属性一样,并没有回调值一说)和“使用变量的属性”(一个属性不能使用自定义属性):

html {
    /* 这是一个CSS自定义属性,它只有一个声明值,并不能有回调值 */
    --main-color: #ec130e;
}

button {
    /* 这是一个CSS变量,它可以有回调值 */
    background-color: var(--main-color, #eee);

    /* 同时使用了两个CSS变量. */
    box-shadow: 0 0 var(--shadow-size) var(--main-color);
}

以及“在元素上声明一个自定义属性”(一个变量没有被声明,而是被分配给一个属性)和“自定义属性的计算值”(一个变量没有计算值,而是从其关联的自定义属性的计算值中提取):

html {
    /* 在html元素上注册了一个CSS自定义属性 */
    --padding: 1em;
}

main {
    /* --padding自定义属性继承到main元素上,它在这个元素上的计算值是16px */
}

main pre {
    /* --padding自定义属性在pre元素上重新注册,该属性在该元素上计算值是8px */
    --padding: 0.5em;
}

其实也可以简单地来区分“CSS自定义属性”和“CSS变量”,即 CSS的var()引用的自定义属性被称为CSS变量

可以在CSS自定义属性中放入什么

可能大家会问,CSS自定义属性中是不是什么东西都可以放呢?似乎是这样,但其中有些东西并不明显,我们来一起看看。

单位值和数学计算

先从最简单的开始。把带有单位的数值放到CSS自定义属性中。比如用来计算元素尺寸大小。

:root {
    --size: 100px;
}

.box {
    width: var(--size);
    height: var(--size);
}

我们还可以在变量中使用calc()来对CSS自定义属性做计算:

:root {
    --w: 400px;
    /* 按宽高比计算--h的值,比如按4:3的宽高比计算 */
    --h: calc(var(--w) / (4/3));
}

.box {
    width: var(--w);
    height: var(--h);
}

这样一来.box就会根据width的宽度按4:3宽高比来调整高度。

同样,CSS自定义属性还可以和其他的一些CSS函数使用,比如min()max()clamp()等函数

:root {  
    --min: 12px;  
    --max: 18px;  
    --clamped-font-size: clamp(var(--min), 2.5vw, var(--max));
}

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

无单位的数值

在CSS中,有些属性的值是不带单位的,比如我们熟悉的z-index,它就不带单位。也就是说,我们在CSS自定义属性中还可以设置不带任何CSS单位的数字值:

:root {
    --modal-index: 999;
}

.modal {
    z-index: var(--modal-index);
}

不带单位的CSS自定义属性的值也可以结合calc()函数使用:

:root {  
    --magic-number: 41;
}

.crazy-box {  
    width: calc(var(--magic-number) * 1%);  
    padding: calc(var(--magic-number) * 1px);  
    transform: rotate(calc(var(--magic-number) * 1deg));
}

更为神奇的是,使用不带单位的数值还可以像@Ana Tudor的教程:

实现truefalse判断(即01间的切换),还可以实现CSS中没有的逻辑运算,比如与(and)、或(or)、非(not)等。比如教程中的or逻辑运算符示例。如果我们的开关变量中至少有一个是1,那么or操作的结果就是1,否则就是0(如果它们都是0)。第一直接就是做加法,但是如果--k--i都是0的话,会得到结果为0;如果其中一个是0,另一个是1的话,会得到结果为2。这并不是or逻辑运算符的结果:

我们可以使用德摩根定律(De Morgan’s laws)中其中一条,即:not (A or B) = (not A) and (not B)

这意味着or操作的结果是--k--i的否定(not)之间的and操作的否定。很烧脑,我们来看具体示例:

--or: calc(1 - (1 - var(--k))*(1 - var(--i)))

随着--k--i取值不同,--or的结果也有差异:

--k --i --or:calc(1 - (1 - var(--k))*(1 - var(--i))) --or结果
0 0 1 - (1 - 0)*(1 - 0) = 1 - 1*1 = 1 - 1 0
0 1 1 - (1 - 0)*(1 - 1) = 1 - 1*0 = 1 - 0 1
1 0 1 - (1 - 1)*(1 - 0) = 1 - 0*1 = 1 - 0 1
1 1 1 - (1 - 1)*(1 - 1) = 1 - 0*0 = 1 - 0 1

使用这些特性就可以做很多事情,比如下面这个示例:

非数字字符

这个和数字字符类似的,在CSS中有些属性会用到非数字的字符串,比如一些颜色的关键词(redblue等),项目符号类型(circle)等;一些自定义的标识符,比如动画名称或网格区域的名称等。来看简单示例:

:root {  
    --bullets: circle;  
    --casing: uppercase;
    --layout-position: center-stage;
}

body {  
    grid-template-areas: 'left center-stage right';
}

main {  
    grid-area: var(--layout-position);
}

li {
    list-style-type: var(--bullets);  
    text-transform: var(--casing);
}

content的内容

了解CSS的同学应该知道,在CSS中可以使用content配合CSS的伪元素::before::after,或者标记符号::marker来生成内容。

同样的,在content中也可以使用CSS自定义属性来填充内容。比如下面这个示例,你将看到如何将字符串变量与其他字符串连接起来,以及如何使用attr()从属性中提取字符串值。

<!-- HTML -->
<main>
    <h1>W3cplus!</h1>
    <p>您阅读过 <a href="https://www.fedev.cn/">我的博客</a>?我将在这里和大家探讨CSS。</p>
</main>

/* CSS */

:root {
    --open: "(";
    --close: ")";
    --heading-hint: " " var(--open) "记述前端那些事,引领Web前沿,打造精品教程"
        var(--close);
    --link-hint: " " var(--open) attr(href) var(--close);
}

h1::after {
    content: var(--heading-hint);
}

a::after {
    content: var(--link-hint);
}

效果如下:

图片

在CSS自定义属性中还可以放置图像,不过需要使用url()函数引用。比如下面这个示例:

:root {
    --bg-image: https://www.fedev.cn/sites/all/themes/w3cplusV2/images/logo.png;
    --logo: url(https://www.fedev.cn/sites/all/themes/w3cplusV2/images/logo.png);
    --bg-color: #00a3cf;
}

div{
    background:var(--bg-color) var(--bg-image) no-repeat center;
}

div:nth-of-type(2) {
    background: var(--bg-color) var(--logo) no-repeat center;
}

自定义属性引用图像地址作为值时,只有url()的才生效:

列表值

记得在Sass处理器中声明变量的时候,可以给一个变量一个列表值,比如:

$lists: '#09f', '#890', '#98a','#fae';

// 或
$lists: ('#09f', '#890', '#98a','#fae');

而CSS中有些属性的值是一个列表值(就是可以同时使用多个值),比如background-image(多背景)、text-shadowbox-shadow(多个阴影)。那么这些多个值就可以用在一个CSS自定义属性中,比如:

:root {
    --single-shadow:
        0 0 0 40px #355c7d;
    --multi-shadow:
        0 0 0 60px #f67280,
        0 0 0 80px #6c5b7b;
    --gradient-colors: #f1bbba, #ece5ce, #c5e0dc;
}

.box__a {
    box-shadow:
        0 0 0 20px #60b99a,
        var(--single-shadow);
}

.box__b {
    box-shadow:
        var(--multi-shadow);
}

.box__c {
    box-shadow:
        0 0 0 20px #60b99a,
        var(--single-shadow),
        var(--multi-shadow);
}

body {
    background-image: linear-gradient(45deg, var(--gradient-colors));
}

CSS Houdini自定义属性的值的并不是什么值都可以

CSS Houdini自定义属性(变量)的值和CSS原生的自定义属生的值并不一样,它的值内容不是什么都可以放的。在前面的内容已提到过,CSS Houdini自定义属性的值是根据syntax值来定义的。它支值的值类型是有限的

这个在《CSS Houdini: @property注册自定义属性》和《CSS Houdini:深入理解CSS自定义属性》有详细介绍过。

在介绍CSS的aspect-ratio的时候,我就犯过这样的错误。因为aspect-ratio接受<ratio>类型的值,我曾尝试着像下面这样来自定义--ratio自定义属性:

@property --aspect-ratio { 
    syntax: "<ratio>"; 
    initial-value: "4 / 3"; 
    inherits: false; 
} 

.aspectratio-container::after { 
    content: ""; 
    width: 1px; 
    padding-bottom: calc(100% / (var(--aspect-ratio)));
    margin: -1px; 
    z-index: -1; 
}

上面的--ratio事实上是不生效的,因为@propertysyntax到目前为止还不支持<ratio>语法类型。而CSS的aspect-ratio除了可以接受/分隔的一对数字值之外,也可以接受一个数值,比如16/91.7777等同。换句话说,如果继续希望使用CSS Houdini的@property让其生效,我们可以考虑将syntax的值更换为<number>,比如:

@property --aspect-ratio { 
    syntax: '<number>'; 
    initial-value: 1.7777; 
    inherits: false; 
}

.aspectratio {
    aspect-ratio: var(--aspect-ratio);
    width: 300px;
}

这个时候可以看到效果:

从这个示例可以得知:CSS Houdini的自定义属性(变量)并不是什么类型都可以放置的,它只能放置syntax目前已得到支持的语法类型值

JavaScript如何操作CSS自定义属性

如果你够仔细的话,应该在前面内容示例中领略到了JavaScript如何操作CSS自定义属性。

在CSSOM中,提供了很多个JavaScript API来对CSS进行操作,其中有些API就可以很好的用来操作CSS自定义属性,比如前面示例中的.setProperty()。除了该API之外还可以使用.getPropertyValue()getCSSCustomProp对CSS自定义属性进行操作。其中.getPropertyValue().setProperty()方法分别是用来获取和重置CSS自定义属性的值。比如:

html {
    --color: #00eb9b;
}

在JavaScript中,我们可以使用getComputedStylegetPropertyValue获取--color自定义属性的值:

const colorAccent = getComputedStyle(document.documentElement).getPropertyValue('--color-accent'); // #00eb9b

如果我们在CSS中改变了--color的值,它也会在JavaScript中更新。但是,当我们在JavaScript中需要访问的不仅仅是一个自定义属性,而是多个自定义属性,会发生什么呢?

html {
    --primary-color: #00eb9b;
    --secondary-color: #9db4ff;
    --text-color: #444;
    --divider-color: #d7d7d7;
}

如果需要获得每个自定义属性的值,可能会像下面这样操作:

const primarColor = getComputedStyle(document.documentElement).getPropertyValue('--primary-color'); // #00eb9b
const secondaryColor = getComputedStyle(document.documentElement).getPropertyValue('--secondary-color'); // #9db4ff
const textColor = getComputedStyle(document.documentElement).getPropertyValue('--text-color'); // #444
const dividerColor = getComputedStyle(document.documentElement).getPropertyValue('--divider-color'); // #d7d7d7

我们做了很多重复的事情。但他们有着共同的特征,可以将其抽象成一个函数,让代码变得更简单:

const getCSSProp = (element, propName) => getComputedStyle(element).getPropertyValue(propName);
const primaryColor = getCSSProp(document.documentElement, '--primary-color'); // #00eb9b

如果你希望动态设备或重新设置一个CSS自定义属性时,可以使用.setProperty(),比如:

document.documentElement.style.setProperty('--color', '#0055fe')

同样我们也可以将其封装成一个函数:

const setCSSProp = (element, propName, value) => element.style.setProperty(propName, value)
const primaryColor = setCSSProp(document.documentElement, '--primary-color', '#0055fe')

有关于JavaScript操作CSS自定义属性更多的介绍,还可以阅读:

对于CSS Houdini中的CSS自定义属性(变量),除了@property属性来注册之外,还可以使用JavaScript的CSS.registerProperty()来注册:

CSS.registerProperty({ 
    name: '--primary-color',
    syntax: '<color>', 
    inherits: false, 
    initialValue: '#0055fe' 
})

使用该特性,可以配合JavaScript的其他特性动态创建多个CSS自定义属性:

if (window.CSS && CSS.registerProperty) {
    document.documentElement.classList.add('supported');
    
    ['x1','x2','y1','y2','z1','z2'].forEach(prop => {
        CSS.registerProperty({
            name: `--r${prop}`,
            syntax: '<angle>',
            inherits: false,
            initialValue: '0deg'
        });
    });
}

上面示例代码来自于@Dan Wilson的在Codepen上写的一个动画效果

浏览器开发者工具中更好的使用CSS自定义属性

在未来我们可以使用浏览器开发者的一些技巧,让我们更容易地使用CSS自定义属性。

查看颜色值

当你使用CSS自定义属性时,看到颜色和背景颜色值的可视化指示器是不是很有用,比如:

计算值

在一些浏览器开发者工具中,开发者将鼠标悬浮或点击CSS自定义属性时,可以查看CSS自定义属性计算值:

CSS自定义属性自动完成

在开发项目时,可能会在项目中同时注册很多个CSS自定义属性,开发者一时可能很难记住这些已注册的CSS自定义属性,这样会阻碍开发者在var()中引用已注册的CSS自定义属性,甚至还有可能会引用未定义或无效的CSS自定义属性。

我们同样可以借助浏览器开发者调试器,浏览器在输入--符号时,会自动弹出CSS自定义属性列表,开发者可以快速定位到自己需要使用的CSS自定义属性:

禁用CSS自定义属性

当你需要禁用某个CSS自定义属性时,可以通过取消选中它所定义的元素来实现:

小结

正如文章中所介绍的,CSS自定义属性又称为CSS变量,时至今日除了原生的CSS自定义属性之外还有CSS Houdini的自定义属性。CSS自定义属性的出现除了能帮助开发者更好的维护和管理自己的CSS代码之外,它还扩展了很多其他的特性,同时也还有很多特性没有被挖掘出来。

关于CSS自定义属性的内容真的很多,网上介绍这方面的特性的教程也很多。但这篇文章和其他文章有着很大的不同之处,文章将CSS自定义属性和CSS Houdini自定义属性结合在一起阐述。让大家更好,更全面的了解这方面的特性。不过,这篇文章更多的是介绍其特性和理论相关的,在下一篇文章将从另一个角度来出发,专门介绍CSS自定义属性的使用场景。通过实际示例来更好的向大家阐述CSS自定义属性的特性和魅力。