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-color
被var()
引用,而--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))
计算出来的transfrom
为none
:
那这就引出了第一个问题:当一个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);
}
你将看到的效果如下:
由于手误,在border
和color
的var()
函数事实上引用了一个并未定义的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
,因此.box
的color
继承了body
的color
值,即#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
禁用,此时border
和color
中的--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
有关,而let
和const
声明的变量作用范围和块作用域有关。
来看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自定义属性作用于该选择器区块内以及其后代元素内(其作用域称为局部或本地)。
很多时候大家都会把:root
和html
等同起来,事实上并非如此,因为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
中声明有关于navbar
、header
和button
相关的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);
}
就上面示例代码,当你改变浏览器窗口的时候,你会发现.card
的margin
会发生相应的变化。即,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代码,body
的font-size
始终都是1em
,哪怕是用户显式设置了.user-setting-large-text
也是如此。但在CSS自定义属性中却不一样,默认body
的font-size
值是1em
,一旦.user-setting-large-text
生效(比如说显式在body
中添加了这个类名或JavaScript给body添加了这个类名),那么body
的font-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中,可以直接进行四则运算(+
、-
、*
、/
和%
)、比较运算(>
、<
、>=
、<=
)、相等操作(==
和!=
)、逻辑运算(and
、or
和not
)以及条件判断@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
的取值为0
或1
做条件判断:
- 当
--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;
}
整个效果如下图:
上面演示的是0
和1
之间的切换,其实还可以非零之间的切换,非零值之间的切换相对而言要更为复杂一些,这里就不做过多的阐述,如果感兴趣的话,可以阅读 @Ana 的两篇博文:
- DRY Switching with CSS Variables: The Difference of One Declaration
- DRY State Switching With CSS Variables: Fallbacks and Invalid Values
我把上面两篇文章整合到一起,可以阅读《如何通过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变量”呢?我们先从相关的文档中来说起。
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 aninitial-value
ofmagenta
.
它的意思是:
你可以使用
@property
API来自定义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的教程:
- DRY Switching with CSS Variables: The Difference of One Declaration
- DRY State Switching With CSS Variables: Fallbacks and Invalid Values
- Logical Operations with CSS Variables
实现true
或false
判断(即0
和1
间的切换),还可以实现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中有些属性会用到非数字的字符串,比如一些颜色的关键词(red
,blue
等),项目符号类型(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-shadow
或box-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
事实上是不生效的,因为@property
中syntax
到目前为止还不支持<ratio>
语法类型。而CSS的aspect-ratio
除了可以接受/
分隔的一对数字值之外,也可以接受一个数值,比如16/9
和1.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中,我们可以使用getComputedStyle
和getPropertyValue
获取--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自定义属性更多的介绍,还可以阅读:
- Making Custom Properties (CSS Variables) More Dynamic
- How to Get All Custom Properties on a Page in JavaScript
对于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自定义属性的特性和魅力。