"2023 CSS新特性系列"是IT深客针对目前浏览器支持程度很高,但是还没有成为正式标准,很有希望成为正式标准的一系列CSS新特性的讲解。希望对想成为CSS达人的你有所帮助。
本文采用阶梯式的介绍方法,由浅入深。
简介
CSS变量顾名思义就是CSS中引入了变量机制。一些重复出现的属性值可以使用CSS变量来代替。由于CSS变量是采用"自定义属性"的方式来定义的,因此W3C给它的正式名字是"CSS自定义属性(Custom properties)"。其实"自定义属性"这个描述更精确一些,但是"变量"更好理解也更顺口。
像Sass的这些CSS预处理器早就开始大量使用变量了,这早已不是一个新鲜的概念。现在终于可以直接在CSS中放心地使用了。
CSS变量的应用举例如下:
css
.main {
--my-favorite-color: #aaa;
background-color: var(--my-favorite-color);
}
这里的--my-favorite-color
就是一个CSS变量,var()
用于读取这个变量,最终background-color
的值是#aaa
。
CSS变量必须以两个横杠--
开头来命名。当要访问这个变量的值时,必须用var()
这个CSS函数。
注意,重要的事情说三遍:
访问变量时一定要用var()
! 访问变量时一定要用var()
! 访问变量时一定要用var()
!
真的很容易忘记加上var()
。
再看一下它的浏览器兼容性(截止到2023年11月):
- IE肯定是不支持。如果你做的是To B的业务,建议当用户使用IE时强行提示用户安装最新的浏览器。
- 所有主流浏览器包括移动端都至少在3年前就已经有了很好的支持。
W3C标准的进程阶段:在2015年就到了CR(候选人推荐)阶段。可以理解为准正式标准。
如果想快速学习,上面这些就够了。
但是如果你想深入使用它,那就还需要了解一下它的性格特点。
作用域
第一个需要了解的特点就是,它是有作用域的。
它只作用于定义它的选择器所指定的HTML元素和其子元素,遵循继承规则。 举例如下:
html
<div class="common specific">
在变量作用域内
</div>
<div class="common">
不在变量作用域内
</div>
<style>
.common {
/* 访问CSS变量 */
color: var(--my-favorite-color);
border: solid 1px blue;
width: 300px;
margin: 20px 20px;
}
.specific {
/* 定义CSS变量 */
--my-favorite-color: red;
}
</style>
specific
类中定义了--my-favorite-color
变量,.common
类使用了该变量,common
类被两个同级的div
使用,但是只有和specific
一起的那个common
中的变量起效果了。因为--my-faforite-color
所在的选择器.specific
只应用在了第一个div
。
再举一个继承关系的例子:
html
<div class="specific">
<div class="common">
在变量作用域内
</div>
</div>
<div>
<div class="common">
不在变量作用域内
</div>
</div>
<style>
.common {
/* 使用CSS变量 */
color: var(--my-favorite-color);
border: solid 1px blue;
width: 300px;
margin: 20px 20px;
}
.specific {
/* 定义了CSS变量 */
--my-favorite-color: red;
}
</style>
这个例子中,虽然两个子div
都有common
类,但是第一个div
的祖先元素的选择器中有--my-favorite-color
变量,因此第一个div
可以继承到该变量。
如果我们想全局的CSS都能使用到变量,那我们就在根元素的样式中定义变量。例如:
css
:root {
--my-favorite-color: red;
}
:root
是代表根元素伪类。也可以直接给html标签定义该样式:
css
html {
--my-favorite-color: red;
}
这样--my-favorite-color
就算是全局CSS变量了。
另外需要说明的是,var()
值的计算发生在继承之前,也就说,对于那些可以被继承的属性,例如color: var(--my-favorate-color)
,被继承的永远是最终var(--my-favorate-color)
的值,而不是这个表达式,所以即使子元素中定义了--my-favorate-color
,它也不会影响到上层已经计算完的color
的值。 举例如下:
html
<style>
:root {
--color: green;
color: var(--color);
}
p {
--color: blue;
}
</style>
<p>绿色</p>
:root
中的color
属性会被继承下来,但是继承下来的不是var(--color)
表达式本身,而是它的值green
,因此p
元素中定义的--color
变量不会影响到p
所继承的color
值。
变量名冲突时的优先级
当页面中有多处名字一样的CSS变量时,如果它们的作用域不重合那就没有影响,但是如果它们的作用域相互重合了,那其规则其实和其他普通属性是一样的。 以下例子来自于W3C的标准草案文档:
html
<style>
/*三个不同的选择器分别给--color变量赋值*/
:root { --color: blue; }
div { --color: green; }
#alert { --color: red; }
/* 给每个元素的color属性都使用了--color变量*/
* { color: var(--color); }
</style>
<p>我继承了根元素的蓝色变量值</p>
<div>我是绿色变量值</div>
<div id='alert'>
我是红色变量值
<p>我也是红色,我继承于祖先元素的变量</p>
</div>
在上面的例子中,用*
通配符给每个元素都设置了color
属性,但是color
的值是一个变量,这个变量的取值要看变量的定义,而本例中有三个变量定义。选择器:root
和div
都是比较泛泛的选择器,他们设置的属性都可以作用于#alert
选择器所代表的元素:一个id为alert
的div
,哪最终哪个选择器里的属性会生效呢?这里就需要用到CSS优先级的概念,一般越具体的选择器,优先级越高,id选择器指代一个具体的元素,它的优先级肯定是要高于:root
和div
这种泛泛的选择器的,因此对于id为alert
的元素,它的--color
变量是red
,而div
选择器又比:root
具体,因此其他div
元素的--color
变量值是green
,然后除此之外且非它们的子元素的标签的值则是:root
定义的blue
。
其实一句话总结,就是多个相同名称的CSS变量时,其优先级规则和普通CSS属性的优先级规则是一样的,当涉及到优先级时,我们完全可以把CSS变量当作一个普通属性看待。
支持!important
另外既然优先级规则和普通属性一样,那!important
也适用于CSS变量。 还是用上面的例子:
html
<style>
/*三个不同的选择器分别给--color变量赋值*/
:root { --color: blue; }
div { --color: green !important; }
#alert { --color: red; }
/* 给每个元素的color属性都使用了--color变量*/
* { color: var(--color); }
</style>
<p>我继承了根元素的蓝色变量值</p>
<div>我是绿色变量值</div>
<div id='alert'>
我也是绿色,因为绿色有!important
<p>我也绿色,因为绿色有!important</p>
</div>
可以看到div
选择器的--color
变量有!important
,因此它的优先级覆盖了#alert
中的--color
变量。 这里的!important
不会被作为变量值的一部分,浏览器在获取--color
的值时,会提前把!important
去掉,所以大家可以放心使用。 另外,!important
一直是一个不怎么被推荐使用的CSS特性,因为它破坏了正常的优先级秩序。
内联样式中也可定义变量
语法:
html
<div style="--my-favorite-color: red"></div>
其变量值只能被当前元素或者其子元素的CSS使用。
大小写敏感
其实一直以来CSS的包容性都比较强,不管是选择器、属性、属性值,都是大小写不敏感,大小写都一样。但是CSS变量不是,CSS变量名是大小写敏感的。例如:--color
变量和--Color
是两个完全不一样的变量。
变量的值可以是变量
既然是变量,那变量自然也可以赋值给另一个变量了。 举个例子:
html
<style>
:root {
--top-bottom: 10px;
--left-right: 20px;
}
div {
--favorite-margin: var(--top-bottom) var(--left-right);
}
<style>
<div style="margin:var(--favorite-margin)"></div>
这让CSS变量更像是普通编程语言中的变量,比较灵活。 但是标准草案文档里也提醒了一种攻击方式:
css
.foo {
--prop1: lol;
--prop2: var(--prop1) var(--prop1);
--prop3: var(--prop2) var(--prop2);
--prop4: var(--prop3) var(--prop3);
/* 等等 */
}
这种攻击的名字叫"十亿大笑攻击"(billion laughs attack,lol全称是"Laugh Out Loud",常在英文聊天中表示大笑)。 --prop1
是lol
--props2
是lol lol
--prop3
是lol lol lol lol
--prop4
是lol lol lol lol lol lol lol lol
这是一种指数级的增长方式,依次写下去,写到--prop31
的时候,该变量将会包含2的30次方个lol
,大概是10亿多个。这可能会吃掉用户大量的内存。因此标准草案文档建议各个浏览器对长度进行限制,但是并没有给出具体长度限制的标准,而是由各个浏览器自行决定。
另外需要说明的是,虽然变量值可以含有var()
表达式,但是var()
值的计算发生在继承之前,也就说,CSS变量在被继承时,被继承的永远是最终的CSS变量值,而不是var()
的表达式。 举例如下:
html
<style>
:root{
--color: gray;
--my-favorite-color: var(--color);
}
p {
--color: red;
color: var(--my-favorite-color);
}
</style>
<p>
灰色
</p>
上面例子中,p
元素样式的color
属性用到了变量--my-favorite-color
,变量值是从:root
继承来的,但是它只会继承--my-favorite-color
的最终值,而不是继承其表达式var(--color)
,因此p
元素样式中的--color
变量不会影响最终的color
属性的值,color
的值还是从上面继承过来的gray
。
基本上有了上面的知识后,我们基本可以自如的使用CSS变量了。
但是如果你还想精益求精(或者说吹毛求疵:)),咱们就继续往下看。
什么是无效值
"无效值"(guaranteed-invalid value),在这里可以理解为其他编程语言中的"null"的意思,它不是空值。 CSS变量的初始值就是无效值。 当var()
引用了一个没有被定义的变量或者所引用的变量作用域不能作用于当前var()
,那var()
所代表的就是一个无效值。 CSS中initial
代表初始值,因此当变量设置成initial
时,则代表此变量的值是无效值:
css
:root {
/* 无效值 */
--my-favorite: initial
}
变量值中一旦有无效值参与,那整个变量就变成无效的了,咱们 可以理解为:任何数据与"null"进行操作,那结果还是"null"。
注意:无效值不是空值。要定义一个空值的变量,可以这样写:
css
--my-favorite: ;
空值和其他数据进行操作,那结果很有可能还是数据本身。 举例如下:
css
:root {
--top-bottom: ;
--left-right: 20px;
--my-favorite-margin: var(--top-bottom) var(--left-right);
}
div {
/* 20px */
margin: var(--my-favorite-margin);
}
这里--top-bottom
是空值,最终div
中margin
的值是20px
。 咱们可以把空值理解为普通编程语言中的空字符。
但是如果上例中的--top-bottom
是一个无效值,那情况就完全不一样了:
css
:root {
--left-right: 20px;
--my-favorite-margin: var(--top-bottom) var(--left-right);
}
div {
/* 无效值 */
margin: var(--my-favorite-margin);
}
此时的margin
属性值是一个无效值,这时浏览器会看该属性默认是否从祖先元素继承,如果不是,那就使用其指定默认值。margin
属性默认不能继承,因此只能使用margin
的默认值,那就是没有margin
。
另外有一点要注意的是,这个例子中的margin: var(--my-favorite-margin)
是一个无效值,但是它不等同于margin: abc;
,margin: abc;
很明显不符合CSS的语法,浏览器在一上来就会把这个错误语法剔除,当它不存在,而无效变量无法被剔除,因为语法上没有错误。 举个例子:
html
<style>
div {
margin: 200px 200px;
}
div {
/* 错误语法会被浏览器忽略,上面的margin生效 */
margin: abc;
}
</style>
<div>margin: 200px 200px; 生效</div>
以上例子中,按理说第二div
的样式会覆盖第一个,但是第二个margin: abc;
有语法错误,margin
没有这样的写法,因此直接被浏览器在前期语法检查时就抛弃了,div
元素的margin
最终会是200px 200px
。
但是使用CSS变量时就不太一样了:
html
<style>
:root {
--left-right: 20px;
/* --top-bottom未定义 */
--my-favorite-margin: var(--top-bottom) var(--left-right);
}
div {
margin: 200px 200px;
}
div {
/* 上面的margin不会生效 */
margin: var(--my-favorite-margin);
}
</style>
<div>没有margin</div>
这里--top-bottom
变量未定义,因此导致--my-favorite-margin
变量值是无效值。但是浏览器在前期并不能检测到该值无效,而且也没有语法错误,浏览器会一直把margin: var(--my-favorite-margin)
当作有效的CSS设置,直到真正开始处理div
元素的样式时,通过分析当前的上下文环境,才发现该值无效,但此时上面那个有效的margin
设置margin: 200px 200px
已经被抛弃了,因为浏览器认为已经有新的属性来覆盖上面的属性。这时浏览器会考虑该属性默认是否是从祖先元素继承,但是margin
默认不是,所以最终div
的margin
没有设置成功。该div
最终是没有margin
。这算是浏览器的算法问题。
同样由于浏览器的算法问题,导致了下面的问题:
html
<style>
:root {
--top-bottom: abc;
--left-right: 20px;
--my-favorite-margin: var(--top-bottom) var(--left-right);
}
div {
margin: 200px 200px;
}
div {
/* 解析后是margin: abc; 导致margin失效*/
margin: var(--my-favorite-margin);
}
</style>
<div>没有margin</div>
这个例子中,变量值倒是有效的,但是它不适用于margin
,但是由于浏览器事先不能提前解析出变量值,只有到最后开始处理div
元素的样式时才发现margin
设置无效,但是此时上面有效的margin: 200px 200px
已经被浏览器抛弃,这时浏览器会考虑该属性默认是否是从祖先元素继承,但是margin
不是,因此导致最终div
没有margin
。 因此通过变量值设置的CSS属性,属性值无效的情况下,依然不会采用上面有效的相同CSS属性,而是会考虑继承或者使用初始值。
initial inherit unset关键字作为值的问题
initial
inherit
unset
是CSS的全局关键字,分别代表初始值、继承、取消继承。CSS变量如果是这些值时,CSS变量并不能将这些值原封不动的带给所赋值的属性。 举个例子:
html
<style>
:root {
/* initial代表自定义属性的默认值 */
--my-favorite-color: initial;
}
div {
/*color的值不是initial*/
color: var(--my-favorite-color);
}
上面例子中--my-favorite-color
变量的值initial
并不能被带给div
的css属性color
,咱们上一节已经讨论过,此时的initial
是有特殊含义的,它代表着当前自定属性的默认值:无效值(null)。当color
遇到无效值时,会考虑从祖先元素继承,而不是color:initial
(浏览器默认值)。 inherit
unset
的情况和initial
一样。 那我们如果确实就是想通过变量把initial
关键字传给color
属性呢? 只能这样:
css
div {
color: var(--my-favorite-color, initial);
}
这里的initial
是备用值,我们下一节会讲。当--my-favorite-color
不存在时,这等同于:color: initial
。
备用值
语法:
css
var(--my-favorite-color, #aaa)
当--my-favorite-color
的值时无效值时,var()
函数会返回备用值,这里的备用值是#aaa
。第一个,
后面的值都会被认为是备用值,那个怕有面还有,
,例如:var(--my-favorite-color, #aaa, #bbb)
,这里的备用值是#aaa, #bbb
。 这个特性对于处理无效值的情况比较有用,它很像普通编程语言中的默认值,只是该默认值并没有赋值给变量本身。 备用值还可以是initial
inherit
unset
:
css
p { color: var(--invalid-value, initial); }
上面的例子中备用值是initial
,前面说过当变量的值设置成initial
时,代表该变量值是无效变量值(null)。但是这里的备用值initial
和那个initial
完全是两回事,这里的initial
是给当前属性color
用的,当--invalid-value
是一个无效值时,上面的语句等同于:
css
p {color: initial}
这句话的意思是,不从祖先元素继承color
,而是使用color
的初始值(根据各浏览器而定)。
循环依赖问题
由于变量的值可以是变量,那就带来了一些问题,如果变量的值是变量自己呢?如果某变量的值是另外一个变量,但是该变量最终还是依赖某变量自己呢?这就形成了循环依赖。
把有依赖关系的两个变量之间用线连起来,画成依赖图,如果发现有形成一个圈的,那这个圈就是循环依赖,按照CSS变量的规则,这个圈中的所有CSS变量在通过var()
来获取值时都是"无效值"(invalid at computed-value time),咱们可以把它理解为null
。 举个例子如下:
上图中红色方框形成了一个闭合的圈,这就是循环依赖,处于循环依赖中的变量,在最终用var()
获取值时都是无效值,从而所有依赖他们的变量自然也就都成为无效值了。
如果虽然有循环依赖,但是如果设置了"备用值"呢? 答案是,依然是会是无效值。 举一个例子如下:
html
<style>
:root {
--top-bottom: var(--favorite-margin);
--left-right: 20px;
--favorite-margin: var(--top-bottom, 200px) var(--left-right);
}
div {
margin: var(--favorite-margin);
}
</style>
<div>
被测试div
</div>
上面的例子中,--favorite-margin
由--top-bottom
和--left-right
组成,而--top-bottom
又循环依赖了--favorite-margin
。 此时的--top-bottom
会是无效值,但是var(--top-bottom, 200px)
这一句中的200px
并不会被启用,实际结果是--top-bottom
和--favorite-margin
都变成了无效值。因此浏览器一旦检测到循环依赖,那所有相关的变量值就都被抛弃了。
另外需要提醒的是,不在同一层级的CSS变量永远也不会形成循环依赖,因为上层的CSS变量永远也不可能访问到下面层级的CSS变量,CSS变量值是向下继承的。
Javascript的API
Javascript的API和其他属性有一些不一样。下面分情况讨论。
读取
API 1:
javascript
o.style.getPropertyValue('--xxx')
o
可能是DOM元素或者CSSOM下的对象。 DOM元素就是用 document.getElementById()
等API获取到的元素。 CSSOM下的对象访问方式:document.styleSheets[xxx].cssRules[xxx]
,xxx
代表索引。这种方式可以访问到样式表文件中的CSS。
这种方式获取的值是未经过计算的初始值。 例如:
html
<div id="x" style="--color: #abc;--my-favorite-color: var(--color);"></div>
通过document.getElementById('x').style.getPropertyValue('--my-favorite-color')
获取到的是:var(--color)
,var(--color)
前面的空格会被去除。 如果要获取计算后的实际值,需要用下面讲到的API。
自定义属性并不能像普通属性这样:o.style.xxx
或者o.style['xxx']
的形式去访问。只能调用o.style.getPropertyValue('xxx')
函数。 我们可以这样来理解这个问题:o.style
在浏览器内部应该是一个静态类型的实例,这个静态类型一旦被定义自然就不好在运行时动态地加属性了。因此只能通过函数去动态的获取。
API 2:
javascript
getComputedStyle(element).getPropertyValue('variable-name')
element
是DOM元素对象 variable-name
是CSS变量名 此API获取到的变量值是经过计算的最终值。
保存
API :
javascript
o.style.setProperty('--xxx', 'xxx')
o
可能是DOM元素或者CSSOM下的对象。 DOM元素就是用 document.getElementById()
等API获取到的元素。 CSSOM下的对象访问方式:document.styleSheets[xxx].cssRules[xxx]
,xxx
代表索引。这种方式可以访问到样式表文件中的CSS。
all属性对CSS变量无效
all
属性用于重置所有作用于目标元素的CSS属性,但是有几个属性它无法重置,CSS变量就是其中一个。
变量不可以作为属性名或者选择器
css
.foo {
--side: margin-top;
/* 错误语法 */
var(--side): 20px;
}
以上例子中,var(--side): 20px
会被当作错误语法被浏览器抛弃。 CSS变量只可以作为属性值使用。
不支持在动画关键帧中修改CSS变量
CSS变量几乎可以用于任何地方,包括@media
、内联样式等。 但是在动画中浏览器不能做到对CSS变量进行插值,导致动画没有过渡效果:
css
@keyframes my-frame {
0% {
--my-left: 0;
}
50% {
/* 无法进行插值,动画没有过渡效果 */
--my-left: 50px;
}
100% {
/* 无法进行插值,动画没有过渡效果 */
--my-left: 100px;
}
}
动画会从left:0
直接跳到50px
,再跳到100px
。因此关键帧中不能修改CSS变量的值。
但是可以这样:
css
:root {
--my-left: 100px;
}
@keyframes my-frame {
0% {
/* 有效 */
left: 0;
}
50% {
left: 50px;
}
100% {
left: var(--my-left);
}
}
这个例子中,只是使用CSS变量,但是并没有修改,因此可以。
不可以用CSS变量拼凑Token
Token
这个词太抽象,我们直接举个例子(来自标准草案):
css
.foo {
--gap: 20;
/* 错误用法*/
margin-top: var(--gap)px;
}
以上用法最终的margin-top
值是20 px
,而不是20px
,前面这个值对于CSS来说是错误语法,会被浏览器抛弃。
再举一个例子:
css
.foo {
--cent: cent;
/* 错误用法*/
text-align: var(--cent)er;
}
这个也是不可以的。center
是一个完整的CSS Token,不可以被拆开。
好了,就是这些了。CSS变量看似简单,但毕竟它给CSS带来了一定的普通编程的特性,可能会让我们的代码变得更复杂,但是也更灵活。