CSS变量 的 秘密
无意间翻刷了CSS Day 2022上Lea Verou 介绍 CSS Variable 的视频, 不得不感慨CSS已经快变成我不认识的样子了。结合视频中的几个案例聊聊CSS变量。
1 初识CSS变量
如果你之前使用过诸如Less, Sass的预编译的CSS扩展,那你一定对其中的变量系统有印象,比如Sass使用$
声明变量:
$base-color: #c6538c;
$border-dark: rgba($base-color, 0.88);
1.1 CSS变量的定义
其实在原生的CSS中也可以使用--
来声明变量,如--color: pink
。声明完成之后可以通过var()
来调用对应的变量,一个简单的例子如下:
button {
--color: green;
border: 1px solid var(--color);
color: var(--color);
/* 其他不重要的装饰样式 略*/
}
在这个例子里面,我们通过--color
定义了color
变量为绿色,并通过var(--color)
将该值应用到按钮的边框和颜色上,看到的结果如下:
通过var()
调用CSS变量的时候,我们也可以指定一个备选值,如果变量不存在的话,CSS就会调用这个备选值。比如在下面的案例中,我们尝试调用没有声明的变量--color2
,并指定备选颜色是橙色:
#button_fallback {
border: 1px solid var(--color2, orange);
color: var(--color2, orange);
}
最终结果显示的就是备选的橙色
1.2 通过JS修改CSS变量
当然我们也可以通过JS去设置CSS变量,即通过style.setProperty()
方法来设置css变量,比如我们每次在按钮被点击的时候随机设置一种颜色:
<button onclick="this.style.setProperty('--color', `hsl(${Math.random() * 360} 90% 50%)`)">Click me</button>
那么,他运行起来的效果就是这样:
1.3 CSS变量的变量类型
与 动画
CSS有了变量,为什么还需要定义变量类型呢?这是多此一举么?我们可以通过一个动画的例子来说明这个问题。
在这个例子中我们给一个按钮(id="button_example4")设置一个5s的颜色改变的动画,并在动画中修改颜色的hue值从0到180。
@keyframes color-hue {
from { --color-hue: 0; }
to { --color-hue: 180; }
}
#button_example4{
--color: hsl(var(--color-hue) 60% 50%);
animation: 5s color-hue linear infinite;
}
按照我们的预想,这个动画应该会在5s内从颜色A渐变到颜色B,但结果却是:
颜色并没有渐变,而是每5s颜色直接跳跃成另外一种颜色。这是由于我们并没有指定color-hue
变量的类型,CSS并不知道color-hue
应该是数值还是文本类型的值(如red
,blue
等)。
1.3.1 通过@property
定义CSS变量的类型
如果想要声明变量的类型,我们可以通过@property
来定义,在上面的例子中中,我们加入对--color-hue
的类型声明:
@property --color-hue {
syntax: "<number>";
initial-value: 0;
inherits: true;
}
这里声明它的类型syntax
是数值<number>
,初始值initial-value
是0
,我们再来看一下效果:
完美的渐变过程!
除了<number>
以外,我们也可以使用其他的类型,比如通过以下的方式来指定<color>
类型的值:
@property --color {
syntax: "<color>";
initial-value: black;
inherits: true;
}
除了在CSS中定义变量的类型,我们也可以通过JS来的CSS.registerProperty()
API来定义,比如:
window.CSS.registerProperty({
name: '--color',
syntax: '<color>',
inherits: false,
initialValue: black,
});
完整的例子:
2 CSS变量的魔力
2.1 利用CSS变量的运行时检查
来制作样式的开关
我一直觉得下面的例子很神奇(交互Demo在本节底部)。我们可以像使用JS一样,通过调用ON
和OFF
变量来控制按钮是否使用高光效果 - 当我们把glossy
设置为OFF
的时候,高光效果消失,反之亦然:
是不是很有意思?这主要运用了 CSS运行时检查 的一个hack,全名叫 Invalid At Computed Value Time (IACVT)。 顾名思义,当CSS发现变量值为不可用的值的时候,会忽略当前内容并将该条的CSS设置成unset
状态 - 即无效状态。下面的一些情况都会触发IACVT。
值类型不正确
--foo: 42deg; background: var(--foo);
这里我们虽然定义了变量
foo
,但是由于background的属性不能为角度,所以最终结果是background: unset;
未初始化或者值为空(
)的变量
background: var(--foo);
--foo: ; background: var(--foo);
以上两种情况,都会导致background无效即
background: unset;
引用了不可用的值
--foo: var(--bar); --bar: var(--foo);
循环引用,但是--foo和--bar均未被正确的赋值,所以两个的结果都会变成
unset
知道了以上内容,再结合var()
的fallback机制 - 当var的第一个参数为guaranteed-invalid值的时候并且我们提供了第二个参数,var会调用第二个fallback的参数。我们就可以制作成带有 ON
和 OFF
的CSS功能了:
首先我们定义
ON
和OFF
button { --ON: initial; --OFF: ; }
这里我们使用
initial
初始化ON
并用空白字符串把OFF
定义为无效的CSS。利用
var()
的fallback配置border
,backgrond
等button { border: var(--glossy, .05em solid black); background: var(--glossy, linear-gradient(hsl(0 0% 100% / .4), transparent)) var(--_color); box-shadow: var(--glossy, 0 .1em hsl(0 0% 100% / .4) inset); line-height: calc(1.5 var(--glossy, - .4)); }
由于
initial
是一个guaranteed-invalid值,当var的第一个参数为guaranteed-invalid
的时候,var会去调用fallback的值。所以当我们把glossy
设置成ON
的时候,border就变成了var(initial, .05em solid black)
自然应用的就是.05em solid black
了,但是当我们把glossy
设置为OFF
的时候,由于空值是一个有效值valid (empty) value,所以 border:就不会起作用。同理对于
line-height
也成立,当glossy
为ON
的时候line-height就是1.5 - .4
,当glossy
为OFF
的时候该条样式不生效
体验Demo:
3. 逆天的CSS变量
我只能用逆天来形容接下来看到的Demo(Demo在本章节的底部)- 通过一个变量p
就可以控制柱状图的高度、文本数值以及背景颜色,而且全程没有用到一行JS
这个案例中我觉得有以下几点比较有意思:
- CSS变量控制柱状条的高度
- CSS变量控制文本显示(如何处理单位符合
%
,如何做到四舍五入) - CSS变量控制柱状条的背景颜色
我们一个一个看过来:
3.1 CSS变量控制柱状条的高度
这应该是一道送分题,忽略基础样式和布局,如果我们使用百分比来显示柱状图的高度的话,那只要将对应的数值转换成百分比的形式即可,如:20
->20%
,18.4
->18.4%
。
如何实现呢?脑海里第一个跳出来的是直接拼接可以么,比如 hight: var(--p)%
,答案是不行的。不过好在CSS的计算函数calc
可以提供数值->数值单位
的转换,我们只要*
对应的1个单位的值即可:
height: calc(var(--p) * 1%);
当然如果需要转换成其他的单位,也可以直接乘,比如转换成px: height: calc(var(--p) * 1px);
顺便提及一下,现阶段从数值转换成单位是很容易的,但是想要从单位转换成数值(如100%
-> 100
),还并不支持。
3.2 CSS变量控制文本显示
百分比的文本我们是通过 div::before
的content
来实现的,但content
并不接受类似于高度的直接转换 calc(var(--p) * 1%)
也不支持直接调用变量,如content: var(--p) "%";
。所以我们想到可以通过counter-reset
来变相的解决我们的问题,于是就有了:
.bar-chart > div::before {
counter-reset: p var(--p);
content: counter(p) "%";
}
但是仔细观察,我没发现如果p
是小数的话,结果居然是0%
这是为什么呢?
有没有可能是和变量类型相关?
那我们尝试手动声明p是数值
@property --integer {
syntax: "<integer>";
initial-value: 0;
inherits: true;
}
.bar-chart > div::before {
--integer: calc(var(--p));
counter-reset: p var(--integer);
content: counter(p) "%";
}
Bingo! 问题解决了!
这里如果我们细看的话,你会发现CSS在转换小数到整数的时候,用的是类似JS的round方法。如果我们需要类似ceil
或者floor
的话,我们可以在calc中去+ 0.5
或者- 0.5
,如--integer: calc(var(--p) + 0.5 );
这里注意符号(+/-)前后要保留空格。
3.3 CSS变量控制背景颜色
背景颜色是一个变化区间,对于所有在数值区域范围内的变化我们都可以通过一个公式来实现 Start + (End - Start) * percent
,这里我们使用的是hsl颜色,计划是将h控制在 50 - 100 的范围内,l控制在 50% - 40%的范围。套用上面的公式,很容易得到下面的CSS
--h: calc(50 + (190 - 50) * var(--p) / 100); /* 50 to 190 */
--l: calc(50% + (40% - 50%) * var(--p) / 100); /* 50% to 40% */
background: hsl(var(--h) 100% var(--l));
体验Demo: