在CSS自定义属性中有效使用无效变量
CSS自定义属性已是CSS中非常成熟的特性了,也是近两年非常受欢迎的特性。它的到来让CSS像其他编程语言一样,可以在编写CSS的时候变量,而且还可以在CSS具备动态计算、逻辑运算和状态切换等特性。特别是CSS Houdini的CSS自定义属性更进一步的扩展了CSS的能力。
就我个人而言,在几年前CSS自定义属性就进入我的世界,而且相关的规范也阅读过好几次。但自从阅读了@Lea Verou的《The --var: ;
hack to toggle multiple values with one custom property》一文之后,我才发现自己遗漏了CSS自定义属性中一个非常关键,而且非常强大的特性。那就是CSS自定义属性中的无效变量,即var()
函数中使用了一个无效的变量。如果我们在使用CSS自定义属性的时候,要是有效的使用了无效变量,它可以帮助我们实现一些非常有意思的功能。比如说状态切换,主题切换等。在接下来,我们就一起来探讨这方面的话题。
前景
@Lea Verou在《The --var: ;
hack to toggle multiple values with one custom property》一文中介绍了CSS自定义一种特性,即:使用一个单一的属性值来开启或关闭多个不同的属性,甚至是多个CSS规则。比如下面这个示例,当你将鼠标移动到按钮上,按钮从一个扁平的效果过渡到一个凸起(带有渐变,高亮,边框)的按钮:
代码很简单:
button {
--is-raised: ; /* off by default */
border: 1px solid var(--is-raised, rgb(0 0 0 / 0.1));
background: var(
--is-raised,
linear-gradient(hsl(0 0% 100% / 0.3), transparent)
)
hsl(200 100% 50%);
box-shadow: var(
--is-raised,
0 1px hsl(0 0% 100% / 0.8) inset,
0 0.1em 0.1em -0.1em rgb(0 0 0 / 0.2)
);
text-shadow: var(--is-raised, 0 -1px 1px rgb(0 0 0 / 0.3));
}
button:hover {
--is-raised: initial; /* turn on */
}
button
在:hover
状态时,自定义属性--is-raised
从一个空字符串(--is-raised: ;
)变成了initial
(--is-raised: initial;
),就轻易地实现了两种状态的UI切换。最初我一直没搞明白这其中的原理是什么?后来重新阅读CSS自定义属性规范时,才发现我自己遗漏了其中最为关键的一个特性,即 无效变量(Invalid Variables)。
需要了解的几个关键信息
在详细介绍CSS自定义属性中“无效变量”之间,我们有几个关键信息需要先了解一下,因为掌握这些关键信息有利于我们更好的理解CSS自定义属性中的“无效变量”所起的作用。也就是,为什么可以使用CSS自定义属性就可以轻易实现“一个单一的属性值来开启或关闭多个不同的属性,甚至是多个CSS规则”。
回退值(Fallback Value)
接触过CSS自定义属性的同学应该都了解,在CSS中使用var()
函数调用CSS自定义属性时,该自定义属性在var()
函数中就变成了CSS的变量:
而var()
函数接受两个参数,其中第一个参数是要替换的自定义属性名;第二个参数,如果提供的话是一个回退值,即当被引用的自定义属性无效时,它被用作自定义属性的回退值。比如下面这个示例:
.element {
color: var(--color, red)
}
就该示例而言,--color
并没有被定义,那么这个时候,var()
将会取其第二个参数red
作为其回退值,并赋值给color
属性。
另外一种情况是,在代码中显式声明了自定义属性,但该自定义属性运用于某些CSS属性上时,它是个无效值,比如:
.element {
--color: 20;
border: 3px double var(--color, red);
color: var(--color, blue);
}
这个示例中我们显式声明了--color
自定义属性,而且其值是20
,对于--color
自定义属性是个有效值,但var()
引用--color
时,--color
是有效的,此时var()
的回退值就不会起作用。此时相当于:
.element {
border: 3px double 20;
color: 20;
}
此对color
和border
是个无效的值,会被忽略。但对于color
属性来说,虽然是无效,但这个时候浏览器在计算时,会继承其父元素的color
值,该示例对应的是浏览器默认文本颜色,即#000
。
乍一看似乎很混乱,但有充分的理由。第一个是技术原因:浏览器引擎在“解析时间”(先发生)处理无效或未知的语法,但变量要到“计算值时间”(后发生)才会被解析。
- 在解析时,无效语法的声明会被完全忽略(回退到之前的声明),而之前的声明会被丢弃
- 在计算值时,变量被编译为无效,但为时已晚(之前的声明已经被丢弃了)
根据规范,无效的变量值和未设置的变量值会像unset
一样解析。
这对于开发者而言是好事,因为它允许我们为提供更复杂的回退值。更妙的是,这允许我们使用null
、undefined
状态来设置所需参数。
另外规范中对“ 要在一个属性的值中替换一个var()
”做出了明确的指导:
- 如果
var()
函数的第一个参数命名的自定义属性被动画污染(animation-tainted),并且var()
函数被用于动画属性(animation
)或它的一个简写属性,那么本算法的其余部分将自定义属性视为具有初始值(initial
) - 如果
var()
函数的第一个参数命名的自定义属性的值不是初始值(initial
),则用相应的自定义属性的值替换var()
函数;否则,则用var()
函数的第二个参数值(当然,var()
要显式设置了回退值)。如果var()
的回退值中引用了任何var()
函数,使用原则是相同的;如果没有var()
没有回退值,那么var()
函数的属性在计算值时是无效的
也就是说:
var()
函数是在计算值时间(Computed-Value)被替换的。如果一个声明,一旦所有的var()
函数都被替换进来,那么这个声明在计算值时间是无效的。
保证无效值(Guaranteed-Invalid Value)
如果自定义属性的值是initial
,那它就是一个保证无效的值(Guaranteed-Invalid Value)。正如前面提到的,使用var()
将一个自定义属性替换为这个值(initial
),将会使引用它的属性在计算值时无效。
这个值序列化为空字符串,但实际上在自定义属性中写一个空值,比如--foo: ;
是一个有效的(空)值,而不是保证无效的值。不管出于什么原因,想要手动将一个变量重置为保证无效的值,只需要使用关键词initial
就可以做到。
说到自定义属性的空值就很有意思了,比如:
:root {
--color: ;
--borderColor:;
}
示例中--color
和--borderColor
自定义属性都没有设置其他的值,唯一不同的是--color
后面紧跟的冒号(:
)和分号(;
)之间有一个空格硬编码(记住,在编码的时候手动敲了一个空格),--borderColor
后面紧跟的冒号和分号之间却没有这个空格。他们同时被var()
函数引用的时候,却有天壤之别,--color
是有一个有效的自定义属性,而--borderColor
是一个无效的,如果var()
函数提供回退值时,那么引用--borderColor
变量的var()
函数将会使用回退值替换--borderColor
设置的值。
:root {
--color: ;
--borderColor:;
}
.element {
border: 3px double var(--borderColor, red);
color: var(--color, blue);
}
示例结果和我们前面描述是一致的。--borderColor:;
是一个保证无效的值(等同于--borderColor: initial
),因此会采用回退值red
作为border-color
的值,所以看到的边框颜色是red
;而--color: ;
是一个有效值,这个时候即使var()
函数提供了回退值blue
,也不会被使用。color
取了一个空值,不过color
会继承其父元素的color
值(此例是#000
),因此你看到的文本颜色是黑色。
虽然在自定义属性中使用--foo:;
方式可以和使用--foo: initial;
让该自定义属性是一个保证无效的值,但使用--foo:;
在可读性上不怎么好,甚至对于不了解该特性的同学来说会以为是一个错语;而使用--foo: initial;
方式对于不了解该技术的同学来说同样会让人感到奇怪。因此,为了提高代码可读性,最好是在后面添加相应的代码注释。
计算值(Computed Value)
了解CSS工作机制的同学都应该知道,CSS属性的最终值会经过四步计算:
- 通过指定来确定值,常称之为 指定值(Specified Value)
- 接着处理得到一个用于继承的值,常称之为 计算值(Computed Value)
- 然后如果有必要的话转化为一个绝对值,常称之为 应用值(Used Value)
- 最后根据本地环境限tmhj进行转换,常称之为 实际值(Actual Value)
其中,指定值的是 通过层叠被处理为计算值 ,例如,URI
被转换成绝对的,em
和ex
单位被计算为像素或者绝对长度。计算一个值并不需要用户代理渲染文档。用户代理规则无法处理为绝对URI
的话,该URI
的计算值就是指定值。
一个属性的计算值由属性定义中Computed Value行决定。当指定值为inherit
时,计算值的定义可以依据继承中介绍的规则来计算。即使属性不适用(于当前元素),其计算值也存在,定义在'Applies To'行。然而,有些属性可能根据属性是否适用于该元素来定义元素属性的计算值
理解起来似乎有点晕,这其实涉及到CSS的基础概念和浏览器(客户端)渲染机制相关的知识。抛开别的不说,浏览器的开发者调试器中有一个名叫“Computed”栏,选中元素时,可以查看到该元素最终被计算出来的CSS属性值:
在计算值时间无效
在介绍保证无效值(Guaranteed-Invalid Value)时,提到另一个术语,即 在计算值时无效。
如果一个声明包含一个引用了具有保证无效值的自定义属性的var()
,比如:
.element {
--color: initial;
color: var(--color, red);
}
或者它使用了一个有效的自定义属性,但在替换了它的var()
函数之后,属性值是无效的,比如:
.element {
--color: 20;
color: var(--color)
}
那么这个声明在计算值时可能是无效的。当这种情况发生时,根据属性的类型,计算出的值是以下几种情形之一:
- 该属性是非注册的自定义属性
- 该属性是一个注册的自定义属性,且具有通用语法,计算的值是保证无效值
- 否则,要么是属性的继承值,要么是它的初始值,分别取决于属性是否被继承,就像属性的值被指为
unset
关键词一样
比如:
:root {
--not-a-color: 20px;
}
p {
background-color: red;
}
p {
background-color: var(--not-a-color);
}
p
元素的background-color
计算值将是transparent
(因为background-color
的初始值是transparent
)而不是red
:
如果自定义属性本身没有设置,或者包含一个无效的var()
函数,也会发生同样的情况。
注意,这和开发者直接在样式中写background-color:20px
的情况不同,因为在样式这样书写会被视为CSS语法错误,会导致该规则background-color
被忽略(丢弃),因此会使用background-color: red
规则。
计算值时间无效的概念之所以存在,是因为变量不能像其他语法错误那样“ 提前失效 ”,所以当用户代理意识到一个属性值无效的时候,它已经把其他的级联值扔掉了(正如该示例中的p{background-color: red}
就被扔掉了)。
CSS继承的机制
CSS继承的机制是 CSS层叠和继承中的概念。在CSS中提供了处理CSS继承的机制,简单地讲就是CSS提供了几个属性值,可以用来处理属性的继承。这几个属性值就是initial
、inherit
、unset
和revert
。其实除了这四个属性值之外,还有一个all
属性值。
另外,CSS的属性有“可继承”和“不可继承”之分,比如font-size
就是可继承的,position
就是不可继承,每个CSS属性在相应的规范文档中都可以查到:
同样的,属性有没有提供相应的初始值,规范中也有相应的描述:
而revert
、unset
和all
相对来说要复杂一点。
revert
表示没有使用任何属性值unset
是initial
和inherit
的结合all
是一个简写属性,其重设除了unicode-bidi
和direction
之外的所有属性至它们的初始值或继承值
这几个值的具体使用,可以参照下图:
上图来自于@Elad Shechter的《How Does CSS Work?》一文。
有关于CSS的继承机制更详细的介绍可以阅读:
无效变量
了解了上面这些基本概念之后,再来理解CSS自定义属性中的“无效变量”就好理解了。规范中这样描述”无效变量“:
当一个自定义属性的值是
initial
时,var()
函数不能使用它进行替换。除非指定了一个有效的回退值,否则会使声明在计算值时无效。
也就是说,当一个自定义属性的值是一个保证无效的值时,var()
函数不能使用它进行替换。即一个声明包含一个引用了具有保证无效值的自定义属性的var()
函数,或者它使用了一个有效的自定义属性,但在替换了它的var()
函数之后,属性值是无效的,那么这个声明在计算值时可能是无效的。当这种情况发生时,属性的计算值要么是属性的继承值,要么是它的初始值,分别取决于属性是否被继承,就像属性的值被指定为unset
关键字一样。
其中原因是继承的标准属性将初始化处理为unset
,除了行为是”从根开始未设置“。而且前面也说过:
级联值在计算值时间无效时就应该被仍掉
比如下面这个示例:
<!-- HTML -->
<div class="element">
<i>Element</i>
</div>
/* CSS */
body {
color: #fff;
}
.element {
--color: red;
}
.element i {
--foo: initial;
--color: var(--foo);
background-color: var(--color, orange);
}
如果<i>
嵌套在div.element
中,那么<i>
元素的background-color
是red
,我们分别来将div.element
和i
中声明的自定义属性打印出来:
在.element i
中声明的--color
是一个保证无效的值,但这个时候background-color
的--color
会引用其父元素声明的--color
,所以看到的最终结果是red
,并不是所期望的orange
。如果<i>
元素不是div.element
的子元素时(或后代元素时),即:
<!-- HTML -->
<div class="element">
<i>Element</i>
</div>
<i>Element</i>
/* CSS */
body {
color: #fff;
}
.element {
--color: red;
}
i {
--foo: initial;
--color: var(--foo);
background-color: var(--color, orange);
}
因为第二个<i>
并不是div.element
的子元素,那它的--color
这个时候和--foo
一样,都是保证无效值,因此会采用var()
函数的回退值,也就是orange
:
如果i
中的样式调整一下:
i {
--foo: initial;
--color: var(--foo, #f36);
background-color: var(--color, orange);
}
这个时候,--foo
是个保证无效值,因此--color
会取var()
的回退值,这个时候,两个i
元素的background-color
都将是#f36
:
将对应的自定义属性的值打印出来:
但又不等同于:
.element {
--color: red;
}
i {
--foo: initial;
background-color: var(--color, var(--foo, #f36));
}
我想你能猜到两个i
元素的背景色是什么吧:
我想,通过这个示例,你对CSS自定义属性中的无效值有更深的了解了吧。如果没有理解的话,可以记住这两点:
- 在同一作用域中,如果自定义属性的值是
initial
,表示该自定义属性是一个保证无效值,那么它将会采用var()
回退值,如果var()
未设置回退值,那么会根据属性的unset
来设置值 - 如果不在同一作用域中,当自定义属性值是保证无效值时,会类似JavaScript事件冒泡机制,向上寻找同名称的自定义属性,如果未找到,则会采用
var()
的回退值,要是未设置回退值将会根据属性的unset
取值;如果向上找到同名称的自定义属性,将会采用父(祖先)同名的自定义属性的值
我们回过头来看@Lea Verou 提供的示例:
button {
--is-raised: ; /* off by default */
border: 1px solid var(--is-raised, rgb(0 0 0 / 0.1));
background: var(
--is-raised,
linear-gradient(hsl(0 0% 100% / 0.3), transparent)
)
hsl(200 100% 50%);
box-shadow: var(
--is-raised,
0 1px hsl(0 0% 100% / 0.8) inset,
0 0.1em 0.1em -0.1em rgb(0 0 0 / 0.2)
);
text-shadow: var(--is-raised, 0 -1px 1px rgb(0 0 0 / 0.3));
}
button:hover {
--is-raised: initial; /* turn on */
}
根据前面介绍的,当--is-raised
的值是个空字符串(
)时,--is-raised
是个有效值,那么:
border
的值是1px solid ;
(solid
后面有一个空格符),border-color
的值为currentColor
background
的就是hsl(200 100% 50%);
(hsl
前面有一个空格符)box-shadow
和text-shadow
的值是none
button
在悬浮状态(:hover
)时--is-raised
的值是initial
,这个时候--is-raised
是一个保证无效值,对应的:
border
的值是1px solid rgb(0 0 0 / 0.1);
,即--is-raised
取了var()
函数的回退值rgb(0 0 0 / 0.1)
background
的值是linear-gradient(hsl(0 0% 100% / 0.3), transparent) hsl(200 100% 50%)
,即--is-raised
取了var()
函数的回退值linear-gradient(hsl(0 0% 100% / 0.3), transparent)
box-shadow
的值是0 1px hsl(0 0% 100% / 0.8) inset, 0 0.1em 0.1em -0.1em rgb(0 0 0 / 0.2)
,即--is-raised
取了var()
函数的回退值0 1px hsl(0 0% 100% / 0.8) inset, 0 0.1em 0.1em -0.1em rgb(0 0 0 / 0.2)
text-shadow
的值是0 -1px 1px rgb(0 0 0 / 0.3)
,即--is-raised
取了var()
函数的回退值0 -1px 1px rgb(0 0 0 / 0.3)
因此你所看到的效果如下:
实现了两种UI效果,在同一个属性上对两个值做了切换。虽然效果出来了,但--is-raised: ;
和--is-raised: initial;
不易于阅读和理解。而且--is-raised
的值是从
到initial
切换的状态(即开(ON
)和关(OFF
))切换,这样的话,可以将上面的Demo改成下面这样:
:root {
--ON: initial;
--OFF: ;
}
button {
--is-raised: var(--OFF);
border: 1px solid var(--is-raised, rgb(0 0 0 / 0.1));
background: var(
--is-raised,
linear-gradient(hsl(0 0% 100% / 0.3), transparent)
)
hsl(200 100% 50%);
box-shadow: var(
--is-raised,
0 1px hsl(0 0% 100% / 0.8) inset,
0 0.1em 0.1em -0.1em rgb(0 0 0 / 0.2)
);
text-shadow: var(--is-raised, 0 -1px 1px rgb(0 0 0 / 0.3));
}
button:hover {
--is-raised: var(--ON);
}
button:active {
box-shadow: var(--is-raised, 0 1px 0.2em black inset);
}
注意:在CSS自定义属性中,注册自定义属性名称时,它是有大小写之分的,因此
--on
和--ON
或者--On
代表的都是不同的自定义属性。
自定义属性中有效使用无效变量案例
上面我们看到的示例是非常简单地。在我们实际开发中,除了UI效果的切换之外,还会有一些状态的切换,比如说iOS的暗黑模式(暗色和亮色的切换),Switch的切换,或者说不同屏幕之下组件字号、间距切换等。接下来,我们就来看看如何使用前面介绍的原理实现这些效果。
先来看下面这个示例:
上图来自于@Rob Stinson在Codepen提供的示例
看到这个效果,你可能会想到换肤或者暗黑模式等。
如果忽略第三部分仅看前面两种状态,那和暗黑模式真没啥区别。实现这样的效果,或许你会先想到通过CSS的媒体查询来切换两种状态:
:root {
/* Light theme */
--c-text: #333;
--c-background: #fff;
}
.theme-container {
color: var(--c-text);
background-color: var(--c-background);
}
@media (prefers-color-scheme: dark) {
:root {
/* Dark theme */
--c-text: #fff;
--c-background: #333;
}
}
不过我们要和大家一起来看,如何使用前面介绍的技术,不依赖媒体查询实现这个效果。为了简化示例的复杂度,先实现下图中黑色和白色(对应的1
和2
)的效果:
以往像下在这样就可以实现两种皮肤色的UI效果:
.nav {
/* Dark */
--dark-color: rgba(156, 163, 175, 1);
--dark-bgcolor: rgba(17, 24, 39, 1);
--dark-active-bgcolor: rgba(55, 65, 81, 1);
/* Light */
--light-color: rgba(55, 65, 81, 1);
--light-bgcolor: rgba(243, 244, 246, 1);
--light-active-bgcolor: rgba(209, 213, 219, 1);
color: var(--dark-color);
background-color: var(--dark-bgcolor);
}
a.active,
a:hover {
background-color: var(--dark-active-bgcolor);
}
.nav.light {
color: var(--light-color);
background-color: var(--light-bgcolor);
}
.nav.light a.active,
.nav.light a:hover {
background-color: var(--light-active-bgcolor);
}
如果我们换成今天所介绍的技术来完成的话,我们可以像下面这样改造:
/* 设置切换开关 */
:root {
--ON: initial;
--OFF: ;
}
/* 默认为Dark */
.nav,
.dark {
--light: var(--OFF);
--dark: var(--ON);
}
/* 默认为Light */
.light {
--light: var(--ON);
--dark: var(--OFF);
}
再回过头来,看我们的示例,颜色有变化的主要是:
主题 | nav 背景色 |
nav 文本色 |
a 当前状态和悬浮状态背景色 |
---|---|---|---|
暗色(Dark) | --dark-bgcolor |
--dark-color |
--dark-active-bgcolor |
亮色(Light) | --light-bgcolr |
--light-color |
--light-active-bgcolor |
将这些自定义属性和前面定义的开关结合起来运用到对应的CSS属性中:
.nav {
color: var(--light, var(--light-color)) var(--dark, var(--dark-color));
background-color: var(--light, var(--light-bgcolor)) var(--dark, var(--dark-bgcolor));
}
同样的方式对a
链接悬浮(:hover
)状态和当前状态(.active
)调整样式:
a.active,
a:hover {
background-color: var(--light, var(--light-active-bgcolor)) var(--dark, var(--dark-active-bgcolor));
}
整个的代码如下:
.nav {
--dark-color: rgba(156, 163, 175, 1);
--dark-bgcolor: rgba(17, 24, 39, 1);
--dark-active-bgcolor: rgba(55, 65, 81, 1);
--light-color: rgba(55, 65, 81, 1);
--light-bgcolor: rgba(243, 244, 246, 1);
--light-active-bgcolor: rgba(209, 213, 219, 1);
color: var(--light, var(--light-color)) var(--dark, var(--dark-color));
background-color: var(--light, var(--light-bgcolor))
var(--dark, var(--dark-bgcolor));
}
a.active,
a:hover {
background-color: var(--light, var(--light-active-bgcolor))
var(--dark, var(--dark-active-bgcolor));
}
/* 设置切换开关 */
:root {
--ON: initial;
--OFF: ;
}
/* 默认为Dark */
.nav,
.dark {
--light: var(--OFF);
--dark: var(--ON);
}
/* 默认为Light */
.light {
--light: var(--ON);
--dark: var(--OFF);
}
相比之下,代码更简洁了(不懂这技术的话,理解起来会更痛苦一点)。如果需要根据设备来自动调整的话,我们只在媒体查询中改变开关值:
.nav {
--light: var(--ON);
--dark: var(--OFF);
}
@media (prefers-color-scheme: dark) {
.nav {
--light: var(--OFF);
--dark: var(--ON);
}
}
因为我的系统默认是暗色系,所以看到的效果如下:
调整系统设置可以看到暗色和亮色的切换:
如果你仅是为了验证效果的话,可以直接在Chrome浏览器中调整主题色的值:
如果为了用户有更好的体验,可以给Demo添加一个切换开关:
简单回顾一下,就拿color
为例吧:
.nav {
color: var(--light, var(--light-color)) var(--dark, var(--dark-color));
}
当--light
取值为var(--ON)
且--dark
取值为var(--OFF)
时:
--light
是一个保证无效值,因此会取var()
的回退值var(--light-color)
,对应的就是rgba(55, 65, 81, 1)
--dark
是一个有效值,因此--dark
会取一个空值
此时,color
的值就是 color: rgba(55, 65, 81, 1) ;
()
右括号后面有一个空格符)。
当--light
取值为var(--OFF)
且--dark
取值为var(--ON)
时:
--light
是一个有效值,此时--light
会取一个空值--dark
是一个保证无效值,因此会取var()
的回退值var(--dark-color)
,对应的就是rgba(156, 163, 175, 1)
此时,color
的值就是 color: rgba(156, 163, 175, 1);
(r
字母前面有一个空格)。
接着我们使用同样的方式来实现三种值的切换。
.nav {
/* Dark */
--dark-color: rgba(156, 163, 175, 1);
--dark-bgcolor: rgba(17, 24, 39, 1);
--dark-active-bgcolor: rgba(55, 65, 81, 1);
/* Light */
--light-color: rgba(55, 65, 81, 1);
--light-bgcolor: rgba(243, 244, 246, 1);
--light-active-bgcolor: rgba(209, 213, 219, 1);
/* Blue */
--blue-color: rgba(165, 180, 252, 1);
--blue-bgcolor: rgba(49, 46, 129, 1);
--blue-active-bgcolor: rgba(67, 56, 202, 1);
color: var(--light, var(--light-color)) var(--dark, var(--dark-color))
var(--blue, var(--blue-color));
background-color: var(--light, var(--light-bgcolor))
var(--dark, var(--dark-bgcolor)) var(--blue, var(--blue-bgcolor));
}
a.active,
a:hover {
background-color: var(--light, var(--light-active-bgcolor))
var(--dark, var(--dark-active-bgcolor))
var(--blue, var(--blug-active-bgcolor));
}
/* 设置切换开关 */
:root {
--ON: initial;
--OFF: ;
}
/* 默认为Dark */
.dark {
--light: var(--OFF);
--dark: var(--ON);
--blue: var(--OFF);
}
/* 默认为Light */
.light {
--light: var(--ON);
--dark: var(--OFF);
--blue: var(--OFF);
}
/* 默认为Blue */
.blue {
--light: var(--OFF);
--dark: var(--OFF);
--blue: var(--ON);
}
你将看到效果如下:
在上面的示例上,稍微调整一下,就可以让用户选择自己需要的颜色,然后让导航根据用户的选择切换颜色:
label.dark {
background-color: var(--dark-bgcolor);
}
label.light {
background-color: var(--light-bgcolor);
}
label.blue {
background-color: var(--blue-bgcolor);
}
#dark:checked ~ div .dark ,
#light:checked ~ div .light ,
#blue:checked ~ div .blue {
box-shadow: 0 0 0 3px #2196f3;
}
.nav {
color: var(--light, var(--light-color)) var(--dark, var(--dark-color))
var(--blue, var(--blue-color));
background-color: var(--light, var(--light-bgcolor))
var(--dark, var(--dark-bgcolor)) var(--blue, var(--blue-bgcolor));
}
a.active,
a:hover {
background-color: var(--light, var(--light-active-bgcolor))
var(--dark, var(--dark-active-bgcolor))
var(--blue, var(--blue-active-bgcolor));
}
/* 设置切换开关 */
:root {
--ON: initial;
--OFF: ;
/* Dark */
--dark-color: rgba(156, 163, 175, 1);
--dark-bgcolor: rgba(17, 24, 39, 1);
--dark-active-bgcolor: rgba(55, 65, 81, 1);
/* Light */
--light-color: rgba(55, 65, 81, 1);
--light-bgcolor: rgba(243, 244, 246, 1);
--light-active-bgcolor: rgba(209, 213, 219, 1);
/* Blue */
--blue-color: rgba(165, 180, 252, 1);
--blue-bgcolor: rgba(49, 46, 129, 1);
--blue-active-bgcolor: rgba(67, 56, 202, 1);
}
#dark:checked ~ .nav {
--light: var(--OFF);
--dark: var(--ON);
--blue: var(--OFF);
}
#light:checked ~ .nav {
--light: var(--ON);
--dark: var(--OFF);
--blue: var(--OFF);
}
#blue:checked ~ .nav {
--light: var(--OFF);
--dark: var(--OFF);
--blue: var(--ON);
}
尝试着选择Demo中的颜色,对应的导航UI就会换肤:
另外,示例中的单选按钮在选中和未选中状态边框上的效果也会有所差异:
可以使用同样的方式来做切换:
label {
--box-shadow: var(--ON);
--box-shadow-active: var(--OFF);
box-shadow: 0 0 0 3px var(--box-shadow, rgba(0, 0, 0, 0.05))
var(--box-shadow-active, #2196f3);
}
#dark:checked ~ div .dark,
#light:checked ~ div .light,
#blue:checked ~ div .blue {
--box-shadow: var(--OFF);
--box-shadow-active: var(--ON);
}
再来看一个示例,使用该方法来实现Switch按钮效果:
:root {
--ON: initial;
--OFF: ;
}
.switch {
--checked-bg-color: #4cd964;
--checked-color: #fff;
--unchecked-color: rgba(0, 0, 0, 0.2);
--unchecked-bg-color: #ff3b30;
background: var(--checked, var(--checked-bg-color))
var(--unchecked, var(--unchecked-bg-color));
color: var(--unchecked-color);
}
#no:checked ~ .switch {
--checked: var(--OFF);
--unchecked: var(--ON);
}
#yes:checked ~ .switch {
--checked: var(--ON);
--unchecked: var(--OFF);
}
#yes:checked ~ .switch label[for="yes"],
#no:checked ~ .switch label[for="no"] {
color: var(--checked-color);
}
在介绍CSS自定义属性的时候,多次提到,使用CSS自定义属性可以实现if ... else
的功能:
事实上,我们使用CSS自定义属性的无效值也可以做if ... else
的判断。不同的是,定义--i
在0
和1
之间切换,需要借助CSS的calc()
做动态计算,但使用自定义属性值是不需要做动态计算。接下来,我们使用CSS自定义属性的无效值来实现上图这个简单的效果:
:root {
--ON: initial;
--OFF: ;
}
.element {
--rotate-switch: var(--OFF);
--rotate-deg: 45deg;
transform: rotate(var(--rotate-switch, var(--rotate-deg)));
}
.element span {
transform: rotate(var(--rotate-switch, -45deg));
}
.element:hover {
--rotate-switch: var(--ON);
}
效果如下:
我们可以使用类似的方式实现其他多个值切换,或者在交互行为带来的状态变化等效果。
为什么要使用CSS自定义属性来做切换
众所周知,对于UI的多态切换,我们常用的方式主要有:
- 借助CSS的伪类选择器,比如
:hover
、:focus
等改变相应CSS属性的值 - 借助CSS的媒体查询,在预期的断点下重新设置CSS样式
- 借助JavaScript的能力,在不同状态下动态更新CSS或者增删
class
就拿文章最开始的示例为例吧,如果按照以前的编码习惯,我们可能会像下面这样编码:
button {
padding: .6em 1em;
border: 1px solid;
border-radius: .2em;
background: hsl(200 100% 50%);
color: white;
font: 600 100%/1 sans-serif;
cursor: pointer;
}
button:hover {
border: 1px solid rgb(0 0 0 / .1);
background: linear-gradient(hsl(0 0% 100% / .3), transparent) hsl(200, 100%, 50%);
box-shadow: 0 1px hsl(0 0% 100% / .8) inset, 0 .1em .1em -.1em rgb(0 0 0 / .2);
text-shadow: 0 -1px 1px rgb(0 0 0 / .3)
}
button:active {
box-shadow: 0 1px .2em black inset;
}
或者像下面这个多个皮肤效果的UI:
会给不同的UI设置不同的类名:
<!-- HTML -->
<nav class="dark">
<!-- 列表项 -->
</nav>
<nav class="light">
<!-- 列表项 -->
</nav>
<nav class="blue">
<!-- 列表项 -->
</nav>
/* CSS */
:root {
/* Dark */
--dark-color: rgba(156, 163, 175, 1);
--dark-bgcolor: rgba(17, 24, 39, 1);
--dark-active-bgcolor: rgba(55, 65, 81, 1);
/* Light */
--light-color: rgba(55, 65, 81, 1);
--light-bgcolor: rgba(243, 244, 246, 1);
--light-active-bgcolor: rgba(209, 213, 219, 1);
/* Blue */
--blue-color: rgba(165, 180, 252, 1);
--blue-bgcolor: rgba(49, 46, 129, 1);
--blue-active-bgcolor: rgba(67, 56, 202, 1);
}
.dark {
background-color: var(--dark-bgcolor);
color: var(--dark-color);
}
.dark a:hover,
.dark a.active {
background-color: var(--dark-active-bgcolor);
}
.light {
background-color: var(--light-bgcolor);
color: var(--light-color);
}
.light a:hover,
.light a.active {
background-color: var(--light-active-bgcolor);
}
.blue {
background-color: var(--blue-bgcolor);
color: var(--blue-color);
}
.blue a:hover,
.blue a.active {
background-color: var(--blue-active-bgcolor);
}
当然也可以通过JavaScript的style.setProperty
动态改变所需要的CSS自定义属性的值:
document.querySelector('.nav').style.setProperty('--color', 'rgba(156, 163, 175, 1)')
document.querySelector('.nav').style.setProperty('--bg-color', 'rgba(17, 24, 39, 1)')
document.querySelector('.nav').style.setProperty('--bg-active-color', 'rgba(55, 65, 81, 1)')
但它们都没有的CSS自定义属性中有效地使用无效变量那么方便。还是拿最开始的按钮为例吧:
从上图中代码的对比来看,虽然最终的效果是一致的,使用CSS自定义属性无效变量实现不同状态的UI效果,所要关注的是,在不同状态下自定义属性,比如示例中--is-raised
值的切换。对于不同状态的CSS属性值都在同一个地方做了描述:
button {
border: 1px solid var(--is-raised, rgb(0 0 0 / 0.1));
background: var(
--is-raised,
linear-gradient(hsl(0 0% 100% / 0.3), transparent)
)
hsl(200 100% 50%);
box-shadow: var(
--is-raised,
0 1px hsl(0 0% 100% / 0.8) inset,
0 0.1em 0.1em -0.1em rgb(0 0 0 / 0.2)
);
text-shadow: var(--is-raised, 0 -1px 1px rgb(0 0 0 / 0.3));
}
在以前的编码方式中,不管是CSS还是JavaScript中,都无法在同一个CSS属性中使用不同状态下的属性值。
另外,使用CSS自定义属性的无效值时,可以让我们在很多场景之下不依赖任何的JavaScript的代码,就可以轻易的实现多态的UI效果。即使是需要借助JavaScript来处理CSS自定义属性的值,也会变得更为容易,因为他只需要关注CSS自定义属性的值是--ON
还是--OFF
,因为其他的变更都在CSS中处理了。
还有就是,我们把不同状态下的CSS都放在一起了,代码的管理和维护都会更集中,即使要修改起来,也更方便。再者,就从实现UI的角度来说,如果不需要依赖任何脚本语言就可以完成状态的切换,那么我更倾向于使用纯CSS来完成。
当然,他也有不足之处,那就是在某种程度来说,开发者理解起来会有一度的难度,特别是对于不了解该技术的开发而言。
小结
前面在不同的文章中分别向大家介绍了CSS自定义属性的基础概念和使用场景,也和大家一起探讨了如何使用CSS自定义属性和其他的函数做一些动态计算、模拟if ... else
的条件判断(类似Switch的状态切换),除此之外,@Ana Tudor 给大家介绍了CSS自定义属性如何做逻辑运算。今天这篇文章中@Lea Verou的引导之下,让我重新获取了CSS自定义属性的另一特性,那就是文章中和大家一起探讨的内容。
在深挖CSS自定义如何有效使用无效变量的过程中,发现自己在阅读规范的时候,还是不够仔细,遗漏了一些有意思的东东,比如文章中介绍的,使用无效变量可以在同一个属性中运用多个值,并且可以完全的进行切换。听起来是不是很神奇,我看到这个的时候,我也觉得很神,甚至一脸蒙逼。如果你刚看到这个也有这个感想的话,那你阅读这篇文章是绝对有收获的。