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中,声明一个变量有多种方式,比如:

1
2
3
4
5
var customProperty
// 或
let customProperty = true
// 还可以
const customProperty = IS_ACTIVE

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

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

1
:root { --primary: #f36; }

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

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

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

1
2
3
4
5
6
body {
color: var(--primary);
}
.button {
--primaryButton: var(--primary);
}

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

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

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

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

1
2
3
4
5
6
7
:root {
--primary: #f36;
}
.button {
background-color: var(--primary, #fff)
color: var(--color, #333);
}

四、CSS 自定义属性的使用

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

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

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

1
2
3
4
5
6
7
8
9
:root {
--font-size: 1em;
}
p {
font-size: var(--font-size);
}
section::after {
font-size:1.5em;
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 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{}中来显式声明自定义属性:

1
2
3
4
5
6
7
8
9
:root {
--primary: #f36;
}
.child1 {
background-color: var(--primary);
}
.child2 {
color: var(--primary)
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
: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的级联特性,也可以将事情变得更容易:

1
2
3
4
5
6
7
8
9
10
11
12
:root {
--color: #333;
}
.card {
color: var(--card);
&:nth-child(2) {
--color: #2196F3;
}
&:nth-child(3) {
--color: #f321ab;
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- HTML -->
<p>我是什么颜色?</p>
<div>我又是什么颜色?</div>
<div id="alert">
我是什么颜色?
<p>我又是什么颜色?</p>
</div>
// CSS
:root {
--color: #333;
}
div {
--color: #2196F3;
}
#alert {
--color: #f321ab;
}

结果用下图来阐述:

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

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

1
2
3
4
5
6
7
8
9
<!-- 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自定义属性则会区分大小写。

1
2
3
4
5
6
7
8
:root {
--COLOR: #fff;
--color: #f36;
}
.box {
color: var(--COLOR);
background-color: var(--color);
}

九、CSS自定义属性命名

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

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

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

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

比如下面这个示例:

1
2
3
4
5
6
7
8
:root {
--color: #f36;
}
.box {
width: var(--w, 100px);
color: var(--color, #fff);
border-width: var(--color, 2px);
}

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

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

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

1
2
3
.card {
padding: -10px;
}

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

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

1
2
3
4
5
6
:root {
--color: 20px;
}
.p {
color: var(--color);
}

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

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

1
2
3
4
5
6
:root {
--color: 20px;
}
p {
color: var(--color, blue);
}

十二、链式的CSS自定义属性

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

1
2
3
4
5
6
p {
--color1: red;
--color2: blueviolet;
--color3: orange;
color: var(--color1, var(--color2, var(--color3, blue)));
}

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

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

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

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

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

1
2
3
4
5
6
7
: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自定义属性