无意间翻刷了CSS Day 2022上Lea Verou 介绍 CSS Variable 的视频, 不得不感慨CSS已经快变成我不认识的样子了。结合视频中的几个案例聊聊CSS变量。
如果你之前使用过诸如Less, Sass的预编译的CSS扩展,那你一定对其中的变量系统有印象,比如Sass使用$声明变量:
$base-color: #c6538c;
$border-dark: rgba($base-color, 0.88);其实在原生的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);
}最终结果显示的就是备选的橙色

当然我们也可以通过JS去设置CSS变量,即通过style.setProperty()方法来设置css变量,比如我们每次在按钮被点击的时候随机设置一种颜色:
<button onclick="this.style.setProperty('--color', `hsl(${Math.random() * 360} 90% 50%)`)">Click me</button>那么,他运行起来的效果就是这样:

变量类型 与 动画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等)。
@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,
});完整的例子:
运行时检查来制作样式的开关我一直觉得下面的例子很神奇(交互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:
我只能用逆天来形容接下来看到的Demo(Demo在本章节的底部)- 通过一个变量p 就可以控制柱状图的高度、文本数值以及背景颜色,而且全程没有用到一行JS

这个案例中我觉得有以下几点比较有意思:
%,如何做到四舍五入)我们一个一个看过来:
这应该是一道送分题,忽略基础样式和布局,如果我们使用百分比来显示柱状图的高度的话,那只要将对应的数值转换成百分比的形式即可,如: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),还并不支持。
百分比的文本我们是通过 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 ); 这里注意符号(+/-)前后要保留空格。
背景颜色是一个变化区间,对于所有在数值区域范围内的变化我们都可以通过一个公式来实现 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:
博客文章会第一时间发布,然后按类型同步到对应公众号
 THE END
THE END
写这篇文章时想了很多问题,不知道你怎么看?