专栏介绍
“顶级写手”是OPPO工程师解读最新热点技术的专栏,在这里你不仅可以看到最新最热的动态,还能和OPPO优秀的工程师一起学习技术知识。
顶级作家
■鲍勃
■一位在前端领域耕耘9年多的前端“新生代IT民工”,立志分享各种前端酷知识。
01
背景
相信用过 UI 框架的同学应该或多或少都修改过它的样式,最常见需要修改的样式应该就是:颜色。大多数情况下,官方框架可能会提供主题配置,比如主题颜色配置(使用 Sass 变量)或者 Ant 主题颜色配置(使用 Less 变量)。你可以定义 @-color(Ant) 或者 $--color-() 这样的变量来定制想要的主题颜色,比如 OPPO 绿色。
但相信大家都经历过这种情况,因为是借助了 CSS 预处理器的能力,而非原生的 CSS 支持,这不仅意味着需要打包工具的支持,也意味着这些变量并不是真正的“变量”。它们在打包时会被替换成 CSS 能够识别的颜色值,无法再通过改变其中一种 - 来改变页面上所有相关的颜色。对于只使用一套 OPPO 绿色的场景来说这还好,但如果要改变动态主题色怎么办?或者像夜间模式这种需要改变颜色的场景怎么办?这对于 CSS 来说其实并不容易。
使用 CSS 不是不可以,只是开发体验不好。比如可以为不同的主题设置不同的 class,并根据这些不同的 class 将所有使用颜色的地方重写。试想一下,业务 CSS 中有 20 个地方用到了主题颜色custom什么意思,每增加一套主题,就需要重写这 20 个地方的颜色定义。这其实也是我们之前常用的方法。
当然,也有办法将浏览器版的CSS预处理器嵌入到页面中,如果你已经在网上使用过这种方法,或者有这样的想法,请务必看完本文,思考是否还有其他方法。
遇到需要动态更换主题的同学肯定都搜索过并了解过CSS自定义变量的概念,如果你还没有用过,不用担心,看完你就懂了。
不知道大家有没有注意到,标题里有一个奇怪的---,和变量定义方式$--color-很像。那么这是什么呢?CSS自定义变量就是这么定义的。
02
基本定义
我们先来看一下基本的定义。CSS自定义变量由两部分组成 - 加一个名称。名称和变量名规则差不多,比如区分大小写(这个和普通的CSS属性名不同,CSS属性名一般不区分大小写),但不同之处在于CSS自定义变量名可以以数字开头,包括纯数字,例如-1就是合法的CSS自定义变量,甚至允许使用汉字,甚至允许使用表情符号。
正如那句老话所说:你能做某件事并不意味着你应该做它。想想当你看到有人在别人的代码中写了 --1: 5px, --2: red,然后毫无理由地到处使用 --1 和 --2 时,试着表达你的感受。
其实 CSS 规范和 MDN 文档中都使用了“自定义属性”这个术语,但是“自定义属性”看起来不如“自定义变量”那么清晰吸引人,所以本文使用了“自定义变量”这个术语。请记住它对应的是“自定义属性”或者规范中提到的与变量相关的术语:“自定义变量”。
这里之所以可以是数字开头,也可以是纯数字,准确的说是因为前面有一个--,这个--不能算是一个变量名,因为实际的名字应该包含整个--。
但是,有了名字并不代表变量就可以用,需要赋值才有用。而且,CSS自定义变量必须存在于CSS的一定的元素规则定义中,不能像全局变量那样写在最外层。当然,全局变量也有相应的定义方式,后面会提到。所以CSS自定义变量的完整定义应该是:
:root {
--custom-variable:
; }
/* 举几个栗子 */
html {
--color-primary: green;
--color-disabled: gray;
--wide-border: 3px;
}
除了在CSS文件、样式属性、对应方法中定义外,请注意,这也是在“某一元素规则的定义”中,因为它只对该属性所在的元素及其子元素有效。
03
使用自定义变量
用法
好的,现在我们有了一个有效的 CSS 自定义变量,如何使用它?CSS 定义了 var() 方法,例如,您可以使用 var(--color-) 来读取它。因此,可以像这样设置具有浅绿色背景的 div 的样式:
div {
background-color: var(--color-primary);
}
var() 方法也支持默认值,当对应变量 或者 value 为 时,会读取默认值。定义很简单,用 隔开,后面的值就是默认值。例如 var(--color-,blue),当 --color- 或者 value 等于 时,会返回 blue。
但请注意,如果有多个逗号,则第一个逗号后面的整个值将作为默认值,而不是用逗号分隔。所以 var(--color-,blue,cyan) 的默认值是 blue, cyan。
在定义自定义变量的时候,还可以使用其他自定义变量。同时,var() 支持多层嵌套,因此默认值也可以是另一个自定义变量。例如以下形式:
div.bordered {
--color-border: var(--color-secondary, green); /* 注意 --color-secondary 并未定义 */
border: var(--wide-border) var(--color-border) solid;
color: var(--color-text, var(--color-disabled, black));
}
看到这里,大家应该知道怎么用了,但是为什么说了这么久,好像跟 CSS 预处理器的变量定义没什么区别。好啦,别着急,我先问大家,还记得 CSS 缩写里的 C 代表什么吗?没错,就是“”:“”。这个词也是 CSS 的独特魅力所在,也是最容易让人混淆的部分。不过,相信看文章的大家应该对 的优先级很熟悉了,如果不熟悉的话赶紧复习一下吧。为什么突然提到它呢?因为 CSS 自定义变量也遵循了 的概念。相同的变量定义会默认继承,优先级高的定义会覆盖优先级低的定义。这也是变量能“动态”改变其值的重要原因。
那么思考一下,如果前面所有的代码块都是在同一个页面中依次定义,而页面中有一个div和一个div,那么它们应该分别如何显示呢?
了解了用法之后,假设你需要为这个页面添加多个主题样式,结合上面提到的级联,你想到怎么添加了吗?没错,我们可以用新样式覆盖变量:
.pink-theme {
--color-primary: pink;
--color-border: deeppink;
}
.gold-theme {
--color-primary: gold;
--color-border: goldenrod;
}
/* 还可以加更多 */
当然如果你不想添加class的话,也可以直接将样式写在对应的标签上来覆盖。
正如文章开头所说,这种动态改变 CSS 中变量的值的功能在 CSS 预处理器中是无法轻易做到的,因为它们的变量定义就像它们的名字一样,都是经过预先处理并放置在 CSS 中的。这也是 CSS 自定义变量相较于预处理器的一大优势。
全局变量
看完前面的使用部分,大家有没有搞清楚如何定义一个全局的 CSS 变量呢?因为自定义变量默认是被继承的,所以简单来说就是把样式放到覆盖范围最广的根元素上,也就是 HTML 中的元素上(其实一般情况下放在那里应该就可以了)。
上例中写了一个 :root 伪类,这个伪类也引用了根元素,在 HTML 中也引用了 html 元素,这有什么区别呢?比如下面的定义中,div 的背景应该是什么颜色呢?
:root {
--color-bg: red;
}
html {
--color-bg: blue;
}
div {
background-color: var(--color-bg);
}
如果你看实际的页面,会发现是红色的。为什么呢?:root 是伪类,所以是类的优先级。回想一下优先级的定义,类的优先级要高于元素类型选择器的 html。
除了这种通过继承来增加覆盖率的形式之外,还有一种定义全局变量的方式,后面会提到。首先我们来看几个问题。
无效值
从上面的用法来看,一般情况下,CSS 自定义变量可以简单理解为在调用 var() 的地方将其值替换为文本,但在实现上还是有些区别的。抛开 CSS 解析计算过程的细节不谈,可以看作是 CSS 解析器忽略了使用自定义变量的属性值的语法检查,即不管该值本身是否可以在对应属性中使用。所以即使给自定义变量传递了非法值,这个属性还是会被正常解析,只是在计算值的时候会出错;这不同于直接写入非法值,CSS 解析器会提前检测到语法错误并忽略这条规则。
这样可能有点让人困惑,我们以MDN上的例子为例,做一个简单的扩展:
:root { --text-color: 16px; }
p { color: blue; }
p { color: var(--text-color); }
div { color: blue; }
div { color: 16px; }
如果你打开 demo 页面,会发现第一行〈p〉是黑色的,而第二行〈div〉是蓝色的。这是因为在使用 CSS 自定义变量的〈p〉定义中,color:var(--text-color)被正常解析,覆盖了之前定义的color:blue(CSS 解析器一般会直接丢弃这个没用的规则定义)。然后在替换-text-color 的时候发现它不是一个值,不是合法的颜色,导致〈p〉元素的颜色定义被非法重置。因为color是继承的属性,所以〈p〉会首先尝试取继承的值。由于 demo 中上没有定义color,所以使用了继承的浏览器默认样式color:black。这里因为color:16px在解析过程中被浏览器认为是非法的而被忽略,所以选择了之前有效的定义color:blue。
无效值重置和 CSS 全局关键字中 unset 的效果一致custom什么意思,即对继承的属性启用 unset 时相当于继承父级,对继承的属性禁用 unset 时相当于初始值。这三个字也是 CSS 的三个全局关键字。另外请注意,这三个关键字在使用自定义变量时也是有效的,将自定义变量值设置给它们时,就意味着对这个自定义变量执行 unset,以及操作:
:root {
--text-color: green;
color: red;
}
div {
--text-color: inherit;
color: var(--text-color);
}
在上面的例子中,div的文字颜色会是绿色而不是红色,因为这意味着自定义变量是从父级继承下来的,也就是--text-color的值被计算为绿色,而不是用color:;替换后续color属性的文字。
另一个具有此功能的关键字可能是臭名昭著的 !。当用于自定义变量时,它还意味着自定义变量本身具有 ! 的优先级,而不是将其替换为文本。我不会为此写一个例子,我留给你自己去尝试。
因为不是文本替换,所以有些地方不能写自定义变量。比如不能写自定义变量来替换 CSS 属性名,也不能把值中的数字和单位分开留学之路,用自定义变量替换:
:root {
--property-name: padding;
--padding:10;
}
p {
var(--property-name):100px;/* 语法错误,忽略 */
}
p {
padding:10px;
padding:var(--padding)px; /* 正常解析但计算值失败,等同于 padding: unset,所以前一条规则会被覆盖 */
}
p.correct {
padding: calc(var(--padding)*1px); /* 注意可以利用 calc() 来实现单位 */
}
注意上面 p 的第二个定义 :10px; 是无效的,因为 var(--)px 在语法上不算无效,但实际计算时发现无法正确计算值,所以相当于 unset 了。不过如果你确实想只定义一个数字,并且在使用时加上单位,也可以使用例子中的 calc() 方法来实现。
虽然您不能像本例一样连接单元,但是您可以使用 CSS 自定义变量将属性值拆分为多个部分值,这将在后面的示例中提到。
循环引用
CSS 自定义变量允许你在 var() 内部嵌入另一个 var(),但不可避免地存在一个问题:
:root {
--margin:10px;
}
div {
--padding: calc(var(--margin)-10px);
margin:var(--margin);
padding:var(--padding);
}
div.cyclic {
--margin: calc(var(--padding)+10px);
}
那么,div.的--和--该怎么解析呢?规范中定义,如果在同一个元素下发生循环引用,则本次循环内的所有自定义变量都等于该值,也就是值。
注意关键字“同一元素”,因为自定义变量默认是继承的,而继承行为发生在值计算之后,所以不同元素下的定义不一定构成循环引用,例如我们将最后一段定义改为div下的p:
:root {
--margin:20px;
}
div {
--padding: calc(var(--margin)-10px);
margin:var(--margin);
padding:var(--padding);
background:#f00;
}
div.cyclic p {
--margin: calc(var(--padding)+10px);
margin:var(--margin);
background:#0f0;
}
在这个例子中,div.p的--可以正常计算得到20px,因为p在计算--时,--继承自div,而div已经被计算为10px了。
更多示例
前面的例子中提到,CSS 自定义变量在取值时基本相当于文本替换,没有类型的概念。这样的操作在某些场景下也能出乎意料的方便,比如 CSS-Trick 上提到的例子:
button {
--h: 100;
--s: 50%;
--l: 50%;
--a: 1;
background: hsl(var(--h) var(--s) var(--l) / var(--a));
}
button:hover { /* Change the lightness on hover */
--l: 75%;
}
button:focus { /* Change the saturation on focus */
--s: 75%;
}
button[disabled] { /* Make look disabled */
--s: 0%;
--a: 0.5;
}
甚至有一种方法可以使用自定义变量作为开关,但请谨慎使用......
这里只是给出一些简单的使用示例,相信很多同学在实际工作中,包括我们的项目中,都有自定义变量的情况。
04
关于动画
自定义变量是 CSS 属性,因此可以在动画中使用。但是,由于它们没有类型,CSS 解析器不知道如何应用动画样式。效果不会像您预期的那样:
.color-div {
--angle: 0deg;
background: linear-gradient(var(--angle), red, yellow, blue, purple);
animation: rotate 5s ease-in-out both alternate infinite;
}
@keyframes rotate {
to {
--angle: 180deg;
}
}
打开这个 demo 你会发现背景并没有“动”,只是颜色跳动了一下。其实这应该符合预期,毕竟渐变算是图片,本身不支持动画。
其实一眼就能看懂这段 CSS 代码想要表达什么,但是替换的方式让自定义变量支持放在过渡动画、动画样式中,其实并没有什么用。不过这么明显的问题,规范已经考虑到了,那么接下来我们看看他们是怎么解决这个问题的呢?
定义全局变量的另一种方法(注册)
鉴于CSS自定义变量的语法非常松散,无法定义其值类型、是否继承、及其初始值,这也导致无法很好实现动画等问题,CSS增加了@来定义,或者更准确的说,使用(注册)一个CSS自定义变量,可以设置其类型(或者其遵循的语法)、是否继承、及其初始值-value。结合上面的例子,我们来简单讲解一下@的用法以及一些需要注意的地方。前面的例子只要加上这个定义声明,就可以达到预期的效果了~
@property --angle {
syntax: "
" ;inherits: false;
initial-value: ‘0deg’;
}
同时@也有一个等价的接口可以调用,比如之前的声明,可以使用如下方式实现:
CSS.registerProperty({
name: '--angle',
syntax: '
' ,inherits: false,
initialValue: ‘0deg’;
});
从上面的示例和解释来看,这些属性的含义可能非常简单。 但是,使用它们时需要注意以下几点:
●首先,与大多数@语句一样,@目前必须出现在CSS的最外层,不能嵌套在其他样式声明中,也不能位于最内层(但这种行为可能会改变,规范正在讨论中)。它可以嵌套在条件@语句中,例如@media:
:root {
@property --primary-color {
/*不会生效*/
}
}
@media (min-width: 1200px) {
@property --width {
/* 有效 */
}
}
●其次,所有属性都应设置,否则整个定义将被忽略。唯一的例外是,当属性为*时,-value可以留空。不过,带*的自定义变量的行为与普通定义基本相同。
●当需要填写 -value 时,其值必须符合定义的语法,并且必须是计算独立( )值,否则整个定义将被忽略。什么是“计算独立”?简单来说,就是不再依赖于其他 CSS 属性值。例如,1em 不符合条件,因为它依赖于 font-size 的定义,var(--other-) 也不起作用,但像 1px 和 #F00 这样的值是可以的。
@property --width {
syntax: "
" ;inherits: true;
initial-value:
}
@property --width {
syntax: "
" ;inherits: true;
initial-value: 1rem; /* 依赖根元素font-size,不符合条件,整个定义被忽略 */
}
这里还要说一下-value,由于它的存在,var()的替换和之前略有不同,我们先来举一个上一篇文章中的例子:
:root { --text-color: 16px; }
p { color: blue; }
p { color: var(--text-color); }
div { color: blue; }
div { color: 16px; }
在这个例子中,如前所述,由于计算值时出现错误,定义未设置,因此 〈p〉 的颜色变为黑色。但是如果我们添加定义:
@property --text-color {
syntax: "
" ;inherits: true;
initial-value:
}
这种情况下,在计算值的时候, --text-color: 16px; 并不符合声明定义的语法,相当于 --text-color: unset;,所以结果为 --text-color: #f00; (即初始值),所以〈p〉 会显示为红色,而不是黑色。但请注意,验证是在计算 div 值时发生的,而不是在解析 CSS 时发生的,所以如果我们将 --text-color 的定义改为如下形式,效果还是一样:
:root {
--text-color: #0f0;
--text-color: 16px; /* 依旧是unset,而不是选择读取前一条,因为不是在解析时判断的语法错误 */
}
由于种种原因,规范选择在解析时不做语法检查,所以即使计算时有语法检查,也和常规的解析错误不一致。如果这句话你还不太理解,回想一下前面错误检查的例子中,div文字是蓝色的。
另外要注意,对于有初始值的全局变量,当使用 var(--, ) 时,即使你手动将 --:; 替换为 var(--, ),也不会读取相应 -value 定义的初始值。
可以填写的类型有很多,也支持复合类型。前面的“”,或者“”和“>”等可以猜到的名字都是合法的值。详细列表请看这里。注意引号是必须的,因为这个值需要类型。
关于多个同名声明的优先级,如果CSS定义中存在同名的@声明,则最后一个生效;但如果存在多个同名声明,则其优先级最高,并且该方法不允许重复声明相同的变量名。
最后,目前没有办法取消注册变量,但规范提到可能会稍后添加此功能。
好了,本文到此结束。感谢您花时间阅读。希望您阅读完本文后对 CSS 自定义变量有更好的了解。