CSS条件变量
我将从这里说起: 在W3C标准规范中,使用CSS变量并没有任何条件。我认为这是规范文档中的一个很大的缺陷,CSS变量已经实现了很多我们之前无法实现的功能,并且我们未来可能大量使用,缺失文档描述着实让人沮丧。
但是如果现在我们需要这些对CSS变量的描述怎么办?好,借助其他的CSS知识,我们可以在一些实例中略窥一二。
问题定义
我们需要的是使用一个单一的CSS变量来给不同的CSS属性设置不同的值。这些值并不会基于CSS变量(因为这些值并不会由我们的变量计算得出)。我们需要的是条件。
使用二进制计算
长话短说, 我先列出一些解决策略,然后再详诉。
:root {
--is-big: 0;
}
.is-big {
--is-big: 1;
}
.block {
padding: calc(
25px * var(--is-big) +
10px * (1 - var(--is-big))
);
border-width: calc(
3px * var(--is-big) +
1px * (1 - var(--is-big))
);
}
在这个例子中,我们给所有.block
元素设置 10px
内边距(padding
)和 1px
(border-width
) 的边框,条件是作用在那些元素上的 --is-big
变量不为1
,否则,元素内边距和边框分别为 25px
和 3px
。
CSS变量机制也很简单: 我们在calc()
使用了一个CSS变量的两个可能值,--is-big
要么为0
,要么为1
。换句话说,.block
的内边距要么为 25px * 1 + 10px * 0
(25px
) 要么为 25px * 0 + 10px * 1
(10px
)。
更复杂的情况
我们可以用这种方法去选择CSS变量可能超过2
个值。相应地,每增加一个可能值,表达式也就变的越复杂。选择3
个值进看起来像这样。
.block {
padding: calc(
100px * (1 - var(--foo)) * (2 - var(--foo)) * 0.5 +
20px * var(--foo) * (2 - var(--foo)) +
3px * var(--foo) * (1 - var(--foo)) * -0.5
);
}
--foo
的值可能是0
,1
或者2
。padding
的值也相应的可能为100px
,200px
或者3px
。
原理是相通的: 我们只需要将这个表达式和每一个可能值相乘就好了,结果为1
的是我们需要的,其他则为0
。组成这个表达式也很简单。我们只需要使另外的可能值生效就好了。设计好之后,我们需要添加一个触发值以此来调整结果以致于它等于1
,就是这样了。
可能存在的陷阱
随着计算式越来越复杂,有可能会出错。为什么?规范中是这样描述的:
UAs 支持至少
20
种calc()
表达式,number,dimension 或者percentage 都是表达式。如果calc()
包含了它所支持以外的表达式,它会把这些表达式视作无效。
当然,我自己在浏览器测试了一下,并没有这样的问题。但是你写了非常复杂的代码仍然有这样的可能。或者在将来某些浏览器会有这样的问题,因此当使用非常复杂的计算式时,请小心为上。
根据条件获取颜色
正如你看到的,这些计算表达式只能用在那些可计算的属性上面,因此我们无法使用它改变display
的值或者计算一些非数字的属性。但是,对于颜色(color
)呢?事实上,我们能计算颜色的各个组成部分。不幸的是,现在只有webkit和blink 浏览器引擎能支持颜色的计算,因为火狐现在还不支持在calc()
里使用rgba()
和其他颜色函数。
但是当浏览器支持的时候(或者你在支持颜色计算的浏览器上想体验一下),我们可以这样做。
:root {
--is-red: 0;
}
.block {
background: rgba(
calc(
255*var(--is-red) +
0*(1 - var(--is-red))
),
calc(
0*var(--is-red) +
255*(1 - var(--is-red))
),
0, 1);
}
这里我们把lime
颜色设置成默认值,如果--is-red
被设置成1
,那么得到的颜色就会变成red
(**注意:**如果我们完全忽略CSS表达式,颜色的各个部分则为0
,这使我们的代码更简洁,在这里,我尽量让CSS变量计算变得更清楚)。
借此你对于可计算的属性可以做任何运算,甚至可能创造出任何颜色(包含渐变?你可以试试)。
规则中另一个陷阱
当我们在测试如何产生我们想要的颜色时,我发现了在规范文档中存在一个非常奇怪的现象,它被叫做"类型检查"。我现在很讨厌它。意味着属性值只接受整数值(<integer>
),如果你在calc()
里存在除法运算或一些非整数,即便结果是整数,"处理后的类型"也不会是整数,它可能是数字,意味着我们无法给属性设置我们写入的值。当这些计算式包含两个可能值的时候,我们需要一个非整数指示器。当我们使用颜色或者其他只允许整数的属性时(像z-index
)时能够使表达式无效。比如:
calc(255 * (1 - var(--bar)) * (var(--bar) - 2) * -0.5)
当rgba()
里时使用这个calc()
是无效的。最开始的时候我认为这是个bug,尤其是知道rgba()
可以接受超过255
的值(rgba(9001, +9001, -9001, 43)
得到的是一个正常的黄色(yellow
)),但是这种类型检查似乎对于浏览器来说太难了(因为乘了个0.5
,非整数)。
能否解决?
这里有一种远不完美的解决策略。因为我们知道我们想要的值和非整数指示器,我们只要先计算出来然后四舍五入一下。嗯,这意味着结果在某些时候可能不太精确,但总好过什么都没有吧。
但是这里还有一种策略可以应对颜色,我们可以使用hsla
,而不是rgba
,因为hsla
不接受非整数,而是接受数字和百分比。因此在类型检查中不存在冲突。但是对于特定的属性例如z-index
,还是没辙。因为即便这样,如果从rgb
转到hsl
还是会存在精度的损失。但是这相对于前面的方法还是损失小些。
预处理
当存在分支时,我们仍然可以手写CSS。但是当我们遇到非常复杂的条件时,或者当我们要拿到颜色时,我们最好使用工具这样可以让书写变的更方便。幸运的是,我们正有这样的预处理器。
这是stylus里如何书写的例子:
conditional($var, $values...)
$result = ''
// If there is only an array passed, use its contents
if length($values) == 1
$values = $values[0]
// Validating the values and check if we need to do anything at all
$type = null
$equal = true
for $value, $i in $values
if $i > 0 and $value != $values[0]
$equal = false
$value_type = typeof($value)
$type = $type || $value_type
if !($type == 'unit' or $type == 'rgba')
error('Conditional function can accept only numbers or colors')
if $type != $value_type
error('Conditional function can accept only same type values')
// If all the values are equal, just return one of them
if $equal
return $values[0]
// Handling numbers
if $type == 'unit'
$result = 'calc('
$i_count = 0
for $value, $i in $values
$multiplier = ''
$modifier = 1
$j_count = 0
for $j in 0..(length($values) - 1)
if $j != $i
$j_count = $j_count + 1
// We could use just the general multiplier,
// but for 0 and 1 we can simplify it a bit.
if $j == 0
$modifier = $modifier * $i
$multiplier = $multiplier + $var
else if $j == 1
$modifier = $modifier * ($j - $i)
$multiplier = $multiplier + '(1 - ' + $var + ')'
else
$modifier = $modifier * ($i - $j)
$multiplier = $multiplier + '(' + $var + ' - ' + $j + ')'
if $j_count < length($values) - 1
$multiplier = $multiplier + ' * '
// If value is zero, just don't add it there lol
if $value != 0
if $modifier != 1
$multiplier = $multiplier + ' * ' + (1 / $modifier)
$result = $result + ($i_count > 0 ? ' + ' : '') + $value + ' * ' + $multiplier
$i_count = $i_count + 1
$result = $result + ')'
// Handling colors
if $type == 'rgba'
$hues = ()
$saturations = ()
$lightnesses = ()
$alphas = ()
for $value in $values
push($hues, unit(hue($value), ''))
push($saturations, saturation($value))
push($lightnesses, lightness($value))
push($alphas, alpha($value))
$result = 'hsla(' + conditional($var, $hues) + ', ' + conditional($var, $saturations) + ', ' + conditional($var, $lightnesses) + ', ' + conditional($var, $alphas) + ')'
return unquote($result)
是的,这里代码比较多,但是这个mixin可以在多个值的条件下产生number 和 colors 的色值。
使用也很简单:
border-width: conditional(var(--foo), 10px, 20px)
第一个参数变量是我们的CSS变量,第二个参数变量是当CSS变量为0
时被应用,第三个参数变量是当CSS变量为1
时被应用。
上面的调用会产生一个适当的条件:
border-width: calc(10px * (1 - var(--foo)) + 20px * var(--foo));
这里有一个更复杂的示例:
color: conditional(var(--bar), red, lime, rebeccapurple, orange)
可能生成你不想手写的代码:
color: hsla(calc(120 * var(--bar) * (var(--bar) - 2) * (var(--bar) - 3) * 0.5 + 270 * var(--bar) * (1 - var(--bar)) * (var(--bar) - 3) * 0.5 + 38.82352941176471 * var(--bar) * (1 - var(--bar)) * (var(--bar) - 2) * -0.16666666666666666), calc(100% * (1 - var(--bar)) * (var(--bar) - 2) * (var(--bar) - 3) * 0.16666666666666666 + 100% * var(--bar) * (var(--bar) - 2) * (var(--bar) - 3) * 0.5 + 49.99999999999999% * var(--bar) * (1 - var(--bar)) * (var(--bar) - 3) * 0.5 + 100% * var(--bar) * (1 - var(--bar)) * (var(--bar) - 2) * -0.16666666666666666), calc(50% * (1 - var(--bar)) * (var(--bar) - 2) * (var(--bar) - 3) * 0.16666666666666666 + 50% * var(--bar) * (var(--bar) - 2) * (var(--bar) - 3) * 0.5 + 40% * var(--bar) * (1 - var(--bar)) * (var(--bar) - 3) * 0.5 + 50% * var(--bar) * (1 - var(--bar)) * (var(--bar) - 2) * -0.16666666666666666), 1);
注意这里没有整数监测的属性,因此这里对于z-index
等属性不会生效。但是它已经将颜色转化成了hsla
,使它们变的有效(尽管只有这种转换只有我们需要的时候,它才有所优势)。另外在这个mixin中我们无法执行的是使用CSS变量作为属性值。这对于非整数的属性也是可以实现的,因为我们的条件表达式中是可以插入CSS变量的。也许,等我有空了,我会让这个mixin接受CSS变量。目前,用本文解释的算法也是可能实现的。
降级
当然,如果你打算真的使用它,你需要使用一个方式来降级。这很容易应对浏览器不支持CSS变量的时候:你可以在条件申明前使用回退值。
.block {
padding: 100px; /* fallback */
padding: calc(
100px * ((1 - var(--foo)) * (2 - var(--foo)) / 2) +
20px * (var(--foo) * (2 - var(--foo))) +
3px * (var(--foo) * (1 - var(--foo)) / -2)
);
}
但是当涉及到颜色时,我们会遇到一个问题:当浏览器支持CSS变量时,任何包含CSS变量的申明都会被认为有效。这意味着在CSS中,我们是不太可能在包含CSS变量的申明中做回退的。
在有效的CSS中,background
会得到一个初始值(initial
),不是在回退中我们提供的。因此我们需要提供在一些场景中使用降级处理——使用@support
来测试除了有效的CSS变量其他值。
在我们的例子中,火狐浏览器中我们需要像这样处理:
.block {
color: #f00;
}
@supports (color: rgb(0, calc(0), 0)) {
.block {
color: rgba(calc(255 * (1 - var(--foo))), calc(255 * var(--foo)), 0, 1);
}
}
这里我们测试一下在color
函数中使用calc()
并且做为color
的值。
这里也可能自动创建回退,但这里我并不推荐你使用预处理器处理回退,因为创建这些东西的复杂度超越了CSS预处理器的能力。
用例
我真的不喜欢为那种一目了然的东西提供使用场景。这里我就简单提一下吧。这里不仅仅是CSS变量的,还有一些生成条件,像calc()
的结果;
- CSS变量应用在那些主题分明的块上是绝佳的。你可能有很多的块,然后要把主题应用在这些块上,你只需要一个像
--block--variant:1
的CSS变量。当我们在不同的主题中想给不同的属性设置不同的值时,CSS变量可能是最好的做法了,否则的话,你可能需要给每一种主题制定一个变量。 - 排版。如果我们在CSS变量中使用
<
,<=
,>
,>=
等判断条件,那么可能会应用不同的规则采用不同同的字体,因此你可以设置不同的行高(line-height
),字体粗细(font-weight
)和基于字号(font-size
)的其他属性。 - 响应式设计。如果对表达式设置以条件,这不就变成媒体查询了嘛。你可以使用
vw
或百分比,决定具体条件下,使用哪个样式。
当然也会有其他的应用场景,发现后一定要告诉我。我很确信还有别的东西,但是我记性不太好,记不住所有曾经我想用CSS做的东西。
未来特性
我真的很希望在CSS规范文档中有具体的描述,而不用依赖calc hack 或使用一些特殊值来应对不能计算的属性值。现在我们不可能有除了严格相等其他之外的条件,因此没有"变量大于x
"或者其他之类的。我不知道为什么我们我能使用这些条件,如果你认识文档开发商,帮我给他们提点意见。我希望他们别以“用JS”或者为什么这样不可能的理由来搪塞我。
本文根据@kizmarh的《Conditions for CSS Variables》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:http://kizu.ru/en/fun/conditions-for-css-variables/。
如需转载,烦请注明出处:https://www.fedev.cn/css/conditions-for-css-variables.htmljordan retro 11 mens boots