这是两部分系列文章的第一篇,它探讨了如何使用 CSS 变量来简化复杂布局和交互的代码编写,并使其更容易维护。本期将介绍该技术适用的各种用例。 第二篇文章 将介绍使用回退值和无效值将该技术扩展到非数值。
如果我告诉你,以下图像中宽屏情况(左)和第二种情况(右)之间的差异仅仅是单个 CSS 声明?如果我告诉你,宽屏情况下奇数项和偶数项之间的差异也仅仅是一个 CSS 声明?
或者说,下面折叠状态和展开状态之间的差异仅仅是一个 CSS 声明?
这怎么可能呢?
好吧,正如您从标题中猜到的,这一切都归功于 CSS 变量的力量。
现在已经有很多关于 CSS 变量是什么以及如何开始使用它们的博文,所以我们不会在这里讨论这些内容。
相反,我们将直接深入探讨为什么 CSS 变量对于实现这些用例和其他用例非常有用,然后我们将详细解释各种用例的“如何”。我们将从头开始逐步编写一个实际示例,最后,您将看到一些使用相同技术的更多演示作为眼球糖果。
让我们开始吧!
为什么 CSS 变量有用
对我来说,CSS 变量最棒的一点是,它们为以逻辑、数学和轻松的方式设置样式打开了大门。
一个例子是 CSS 变量版本的 阴阳加载器,我去年编写了。在这个版本中,我们使用加载器元素的两个伪元素来创建两个半部分。
我们对两个半部分使用相同的 background
、border-color
、transform-origin
和 animation-delay
值。这些值都依赖于一个开关变量 --i
,该变量最初在两个半部分(伪元素)上都设置为 0
,然后我们将其更改为第二个半部分(:after
伪元素)的 1
,从而动态修改所有这些属性的计算值。
如果没有 CSS 变量,我们必须在 :after
伪元素上再次设置所有这些属性(border-color
、transform-origin
、background
、animation-delay
),并且可能出现一些拼写错误,甚至忘记设置其中一些属性。
一般情况下切换的工作原理
在零值和非零值之间切换
在阴阳加载器的特定情况下,我们更改的两个半部分(伪元素)之间的所有属性都从开关的一种状态的零值变为另一种状态的非零值。
如果我们希望当开关关闭时(--i: 0
)我们的值为零,而当开关打开时(--i: 1
)我们的值为非零,那么我们将它乘以开关值(var(--i)
)。这样,如果我们的非零值应该是一个 30deg
的角度值,那么我们有
- 当开关关闭(
--i: 0
)时,calc(var(--i)*30deg)
计算结果为0*30deg = 0deg
- 当开关打开(
--i: 1
)时,calc(var(--i)*30deg)
计算结果为1*30deg = 30deg
但是,如果我们希望当开关关闭时(--i: 0
)我们的值为非零,而当开关打开时(--i: 1
)我们的值为零,那么我们将它乘以开关值的补码(1 - var(--i)
)。这样,对于相同的非零角度值 30deg
,我们有
- 当开关关闭(
--i: 0
)时,calc((1 - var(--i))*30deg)
计算结果为(1 - 0)*30deg = 1*30deg = 30deg
- 当开关打开(
--i: 1
)时,calc((1 - var(--i))*30deg)
计算结果为(1 - 1)*30deg = 0*30deg = 0deg
您可以在下面看到这个概念的说明
在加载器的特定情况下,我们使用 HSL 值作为 border-color
和 background-color
。HSL 代表色相、饱和度和亮度,可以用双锥体(由两个底面粘在一起的圆锥体组成)来最好地直观地表示。
色相围绕双锥体,0°
等于 360°
,在两种情况下都得到红色。
饱和度从双锥体的垂直轴上的 0%
变化到双锥体表面的 100%
。当饱和度为 0%
(在双锥体的垂直轴上)时,色相不再重要;我们对相同水平平面上的所有色相都得到完全相同的灰色。
“相同水平平面”意味着具有相同的亮度,亮度沿双锥体的垂直轴增加,从 黑色
双锥体顶点上的 0%
到 白色
双锥体顶点上的 100%
。当亮度为 0%
或 100%
时,色相和饱和度都不再重要——对于 0%
的亮度值,我们始终得到 黑色
,而对于 100%
的亮度值,我们始终得到 白色
。
由于我们只需要 黑色
和 白色
来表示 ☯ 符号,因此色相和饱和度无关紧要,因此我们将它们归零,然后通过在 0%
和 100%
之间切换亮度来在 黑色
和 白色
之间切换。
.yin-yang {
/* other styles that are irrelevant here */
&:before, &:after {
/* other styles that are irrelevant here */
--i: 0;
/* lightness of border-color when
* --i: 0 is (1 - 0)*100% = 1*100% = 100% (white)
* --i: 1 is (1 - 1)*100% = 0*100% = 0% (black) */
border: solid $d/6 hsl(0, 0%, calc((1 - var(--i))*100%));
/* x coordinate of transform-origin when
* --i: 0 is 0*100% = 0% (left)
* --i: 1 is 1*100% = 100% (right) */
transform-origin: calc(var(--i)*100%) 50%;
/* lightness of background-color when
* --i: 0 is 0*100% = 0% (black)
* --i: 1 is 1*100% = 100% (white) */
background: hsl(0, 0%, calc(var(--i)*100%));
/* animation-delay when
* --i: 0 is 0*-$t = 0s
* --i: 1 is 1*-$t = -$t */
animation: s $t ease-in-out calc(var(--i)*#{-$t}) infinite alternate;
}
&:after { --i: 1 }
}
请注意,这种方法 在 Edge 中不起作用,因为 Edge 不支持 animation-delay
的 calc()
值。
但是,如果我们希望当开关关闭时(--i: 0
)具有非零值,而当开关打开时(--i: 1
)具有另一个不同的非零值呢?
在两个非零值之间切换
假设我们希望元素在开关关闭时(--i: 0
)具有灰色 background
(#ccc
),而在开关打开时(--i: 1
)具有橙色 background
(#f90
)。
我们要做的第一件事是从十六进制转换为更易于管理的格式,例如 rgb()
或 hsl()
。
我们可以手动执行此操作,方法是使用 Lea Verou 的 CSS Colors 或通过 DevTools 之类的工具。如果我们对元素设置了 background
,我们可以通过在 DevTools 中按住 Shift
键并单击值前面的方框(或圆圈)来循环遍历格式。这在 Chrome 和 Firefox 中都有效,但似乎在 Edge 中不起作用。
更妙的是,如果我们使用 Sass,我们可以使用 red()
/ green()
/ blue()
或 hue()
/ saturation()
/ lightness()
函数提取组件。
虽然 rgb()
可能是更广为人知的格式,但我倾向于更喜欢 hsl()
,因为我发现它更直观,并且仅通过查看代码,我更容易了解预期的视觉效果。
因此,我们使用这些函数提取两个值(开关关闭时为 $c0: #ccc
,开关打开时为 $c1: #f90
)的 hsl()
等效值的三个组件
$c0: #ccc;
$c1: #f90;
$h0: round(hue($c0)/1deg);
$s0: round(saturation($c0));
$l0: round(lightness($c0));
$h1: round(hue($c1)/1deg);
$s1: round(saturation($c1));
$l1: round(lightness($c1))
请注意,我们对 hue()
、saturation()
和 lightness()
函数的结果进行了四舍五入,因为它们可能会返回许多小数,而我们希望保持生成的代码简洁。我们还将 hue()
函数的结果除以 1deg
,因为在这种情况下,返回的值是度数值,而 Edge 仅支持 CSS hsl()
函数中的无单位值。通常,在使用 Sass 时,我们可以使用度数值,而不仅仅是 hsl()
函数中的无单位值,因为 Sass 将其视为 Sass hsl()
函数,该函数被编译为具有无单位色相的 CSS hsl()
函数。但是在这里,我们有一个动态 CSS 变量,因此 Sass 将此函数视为 CSS hsl()
函数,该函数不会编译成其他任何东西,因此,如果色相有单位,则不会从生成的 CSS 中删除该单位。
现在我们有了
- 如果开关关闭(
--i: 0
),我们的background
是hsl($h0, $s0, $l0)
- 如果开关打开 (
--i: 1
),我们的background
是hsl($h1, $s1, $l1)
我们可以将我们的两个背景写成
- 如果开关关闭 (
--i: 0
),hsl(1*$h0 + 0*$h1, 1*$s0 + 0*$s1, 1*$l0 + 1*$l1)
- 如果开关打开 (
--i: 1
),hsl(0*$h0 + 1*$h1, 0*$s0 + 1*$s1, 0*$l0 + 1*$l1)
使用开关变量 --i
,我们可以统一这两种情况
--j: calc(1 - var(--i));
background: hsl(calc(var(--j)*#{$h0} + var(--i)*#{$h1}),
calc(var(--j)*#{$s0} + var(--i)*#{$s1}),
calc(var(--j)*#{$l0} + var(--i)*#{$l1}))
这里,我们用 --j
表示 --i
的补值(当 --i
为 0
时,--j
为 1
,当 --i
为 1
时,--j
为 0
)。
上面的公式适用于在任何两个 HSL 值之间切换。但是,在这种特殊情况下,我们可以简化它,因为当开关关闭时 (--i: 0
),我们有一个纯灰色。
在考虑到 RGB 模型的情况下,纯灰色值的红、绿、蓝值相等。
在考虑到 HSL 模型的情况下,色调无关紧要(我们的灰色对所有色调看起来都一样),饱和度始终为 0%
,只有亮度很重要,它决定了我们的灰色有多亮或多暗。
在这种情况下,我们始终可以保留非灰色值(即“打开”状态下的值,$h1
)的色调。
由于任何灰色值(即“关闭”状态下的值,$s0
)的饱和度始终为 0%
,因此用 0
或 1
乘以它始终得到 0%
。因此,鉴于我们公式中的 var(--j)*#{$s0}
项始终为 0%
,我们可以直接删除它,我们的饱和度公式简化为“打开”状态下饱和度 $s1
与开关变量 --i
的乘积。
这使得亮度成为唯一仍然需要应用完整公式的组件。
--j: calc(1 - var(--i));
background: hsl($h1,
calc(var(--i)*#{$s1}),
calc(var(--j)*#{$l0} + var(--i)*#{d1l}))
可以在 此演示 中测试以上内容。
类似地,假设我们希望某些文本的 font-size
在开关关闭时 (--i: 0
) 为 2rem
,在开关打开时 (--i: 1
) 为 10vw
。应用相同的方法,我们有
font-size: calc((1 - var(--i))*2rem + var(--i)*10vw)
好了,现在让我们继续澄清这方面的另一个方面:到底是什么导致开关从打开切换到关闭或反过来?
是什么触发了切换
我们这里有几个选择。
基于元素的切换
这意味着开关对于某些元素是关闭的,而对于其他元素是打开的。例如,这可以通过奇偶性来确定。假设我们希望所有偶数元素都旋转并具有橙色 background
,而不是最初的灰色。
.box {
--i: 0;
--j: calc(1 - var(--i));
transform: rotate(calc(var(--i)*30deg));
background: hsl($h1,
calc(var(--i)*#{$s1}),
calc(var(--j)*#{$l0} + var(--i)*#{$l1}));
&:nth-child(2n) { --i: 1 }
}
在奇偶性情况下,我们为每个第二个项目 (:nth-child(2n)
) 打开开关,但我们也可以为每个第七个项目 (:nth-child(7n)
) 打开它,为 前两个项目 (:nth-child(-n + 2)
),为 除第一个和最后两个以外的所有项目 (:nth-child(n + 3):nth-last-child(n + 3)
)。我们还可以仅为标题或仅为具有特定属性的元素打开它。
基于状态的切换
这意味着当元素本身(或其父级或其中一个之前的兄弟姐妹)处于一种状态时,开关关闭,当它处于另一种状态时,开关打开。在上一节的交互式示例中,当我们元素之前的复选框被选中或取消选中时,开关会翻转。
我们也可以像一个白色链接一样,当它获得焦点或悬停时,它会向上缩放并变成橙色
$c: #f90;
$h: round(hue($c)/1deg);
$s: round(saturation($c));
$l: round(lightness($c));
a {
--i: 0;
transform: scale(calc(1 + var(--i)*.25));
color: hsl($h, $s, calc(var(--i)*#{$l} + (1 - var(--i))*100%));
&:focus, &:hover { --i: 1 }
}
由于 white
是任何亮度为 100%
的 hsl()
值(色调和饱和度无关紧要),因此我们可以通过始终保留 :focus
/ :hover
状态的色调和饱和度,并仅更改亮度来简化操作。
基于媒体查询的切换
另一种可能性是切换是由媒体查询触发的,例如,当方向改变或从一个视口范围切换到另一个视口范围时。
假设我们有一个 font-size
为 1rem
的 white
标题,它在 320px
之前是有效的,但之后它会变成橙色 ($c
),font-size
变为 5vw
并开始随着视口 width
缩放。
h5 {
--i: 0;
color: hsl($h, $s, calc(var(--i)*#{$l} + (1 - var(--i))*100%));
font-size: calc(var(--i)*5vw + (1 - var(--i))*1rem);
@media (min-width: 320px) { --i: 1 }
}
从头开始编写更复杂的示例
我们在这里分析的示例是本文开头展示的展开搜索,灵感来自 这个 Pen,你真的应该看看它,因为代码非常巧妙。
请注意,从可用性的角度来看,在网站上使用这样的搜索框可能不是最好的主意,因为通常人们会期望搜索框后面的按钮触发搜索,而不是关闭搜索栏,但这仍然是一个有趣的编码练习,这就是我选择在这里分析它的原因。
首先,我的想法是用表单元素来实现它。因此,HTML 结构如下
<input id='search-btn' type='checkbox'/>
<label for='search-btn'>Show search bar</label>
<input id='search-bar' type='text' placeholder='Search...'/>
我们在这里做的是最初隐藏文本 input
,然后在它前面的复选框被选中时显示它 - 让我们深入了解它是如何工作的!
首先,我们使用一个基本的重置,并为 input
和 label
元素的容器设置 flex
布局。在我们的例子中,这个容器是 body
,但它也可以是另一个元素。我们还绝对定位复选框,并将其移出视线(在视口之外)。
*, :before, :after {
box-sizing: border-box;
margin: 0;
padding: 0;
font: inherit
}
html { overflow-x: hidden }
body {
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
min-width: 400px;
min-height: 100vh;
background: #252525
}
[id='search-btn'] {
position: absolute;
left: -100vh
}
到目前为止,一切都很好……
查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。
那又怎样?我们必须承认它一点也不令人兴奋,所以让我们继续下一步!
我们将复选框 label
变成一个大的圆形绿色按钮,并使用一个很大的负值 text-indent
和 overflow: hidden
将其文本内容移出视线。
$btn-d: 5em;
/* same as before */
[for='search-btn'] {
overflow: hidden;
width: $btn-d;
height: $btn-d;
border-radius: 50%;
box-shadow: 0 0 1.5em rgba(#000, .4);
background: #d9eb52;
text-indent: -100vw;
cursor: pointer;
}
查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。
接下来,我们完善实际的搜索栏,方法是
- 为其提供显式尺寸
- 为其正常状态提供
background
- 为其获得焦点状态定义不同的
background
和光晕 - 使用等于其
height
一半的border-radius
在左侧圆角 - 清理占位符
$btn-d: 5em;
$bar-w: 4*$btn-d;
$bar-h: .65*$btn-d;
$bar-r: .5*$bar-h;
$bar-c: #ffeacc;
/* same as before */
[id='search-bar'] {
border: none;
padding: 0 1em;
width: $bar-w;
height: $bar-h;
border-radius: $bar-r 0 0 $bar-r;
background: #3f324d;
color: #fff;
font: 1em century gothic, verdana, arial, sans-serif;
&::placeholder {
opacity: .5;
color: inherit;
font-size: .875em;
letter-spacing: 1px;
text-shadow: 0 0 1px, 0 0 2px
}
&:focus {
outline: none;
box-shadow: 0 0 1.5em $bar-c, 0 1.25em 1.5em rgba(#000, .2);
background: $bar-c;
color: #000;
}
}
查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。
此时,搜索栏的右边缘与按钮的左边缘重合。但是,我们希望有一点重叠 - 比如搜索栏的右边缘与按钮的垂直中线重合。鉴于我们有一个 flexbox 布局,容器(我们的例子中的 body
)上具有 align-items: center
,即使我们在两个项目(栏和按钮)之间设置一个 margin
,由这两个项目组成的组合仍然保持水平居中对齐。(在最左侧项目的左侧或最右侧项目的右侧是一个不同的故事,但我们现在不会深入研究它。)
这是一个 .5*$btn-d
减去半个按钮直径的重叠,它等效于按钮的半径。我们将它设置为栏上的负 margin-right
。我们还调整栏右侧的 padding
,以便我们补偿重叠
$btn-d: 5em;
$btn-r: .5*$btn-d;
/* same as before */
[id='search-bar'] {
/* same as before */
margin-right: -$btn-r;
padding: 0 calc(#{$btn-r} + 1em) 0 1em;
}
现在,我们已经将栏和按钮放置在展开状态的位置
查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。
除了栏在 DOM 顺序中跟随按钮,因此它被放置在按钮的顶部,而我们实际上希望按钮在顶部。幸运的是,这有一个简单的解决办法(至少现在是 - 以后就不够了,但让我们一次解决一个问题)。
[for='search-btn'] {
/* same as before */
position: relative;
}
现在我们已经给按钮设置了一个非静态的 `position` 值,它就在条形图的上面。
查看 thebabydino 在 CodePen 上创建的 Pen ( @thebabydino )。
在这种状态下,条形图和按钮组件的总宽度是条形图宽度 `$bar-w` 加上按钮半径 `$btn-r` ( 按钮直径 `$btn-d` 的一半 ),因为我们有半个按钮的重叠。在折叠状态下,组件的总宽度就是按钮直径 `$btn-d`。
由于我们希望在从展开状态到折叠状态时保持相同的中心轴,我们需要将按钮向左移动展开状态下组件宽度的一半 ( `0.5*($bar-w + $btn-r)` ) 减去按钮半径 ( `$btn-r` )。
我们称这种移动为 `$x`,我们在按钮上使用它,并使用减号 ( 因为我们向左移动按钮,而左边是 x 轴的负方向 )。由于我们希望条形图折叠到按钮中,我们对它设置相同的移动 `$x`,但方向为正 ( 因为我们向 x 轴的右边移动条形图 )。
当复选框未选中时,我们处于折叠状态,当它选中时,我们处于展开状态。这意味着我们的条形图和按钮在复选框未选中时通过 CSS `transform` 移动,并在我们当前放置它们的位置 ( 没有 `transform` ) 时,复选框选中。
为了做到这一点,我们在复选框后面的元素 ( 用复选框的 `label` 创建的按钮和搜索栏 ) 上设置了一个变量 `--i`。这个变量在折叠状态 ( 当两个元素都移动且复选框未选中时 ) 为 `0`,在展开状态 ( 当我们的条形图和按钮处于它们当前占据的位置时,没有移动,复选框选中 ) 为 `1`。
$x: .5*($bar-w + $btn-r) - $btn-r;
[id='search-btn'] {
position: absolute;
left: -100vw;
~ * {
--i: 0;
--j: calc(1 - var(--i)) /* 1 when --i is 0, 0 when --i is 1 */
}
&:checked ~ * { --i: 1 }
}
[for='search-btn'] {
/* same as before */
/* if --i is 0, --j is 1 => our translation amount is -$x
* if --i is 1, --j is 0 => our translation amount is 0 */
transform: translate(calc(var(--j)*#{-$x}));
}
[id='search-bar'] {
/* same as before */
/* if --i is 0, --j is 1 => our translation amount is $x
* if --i is 1, --j is 0 => our translation amount is 0 */
transform: translate(calc(var(--j)*#{$x}));
}
现在我们有了交互式内容!点击按钮会切换复选框的状态 ( 因为按钮是用复选框的 `label` 创建的 )。
查看 thebabydino 在 CodePen 上创建的 Pen ( @thebabydino )。
除了现在按钮有点难以点击,因为它再次在文本输入的下面 ( 因为我们已经对条形图设置了 `transform`,这建立了一个堆叠上下文 )。修复方法很简单,我们需要给按钮添加一个 `z-index`,这会将它移到条形图的上面。
[for='search-btn'] {
/* same as before */
z-index: 1;
}
查看 thebabydino 在 CodePen 上创建的 Pen ( @thebabydino )。
但我们还有另一个更大的问题:我们可以看到条形图从按钮的右边伸出来。为了解决这个问题,我们在条形图上设置 `clip-path`,并使用 `inset()` 值。这通过使用元素 `border-box` 的顶部、右侧、底部和左侧边缘的距离来指定一个剪切矩形。这个剪切矩形之外的所有内容都会被剪掉,只有里面的内容会被显示。
在上面的图示中,每个距离都是从 `border-box` 的边缘向内延伸。在这种情况下,它们是正数。但它们也可以向外延伸,在这种情况下,它们是负数,剪切矩形的相应边缘位于元素的 `border-box` 外部。
一开始,你可能认为我们永远不会有必要这样做,但我们的特殊情况下,我们需要这样做!
我们希望顶部 ( `dt` )、底部 ( `db` ) 和左边 ( `dl` ) 的距离为负数,并且足够大,可以容纳在 `:focus` 状态下扩展到元素 `border-box` 外部的 `box-shadow`,因为我们不希望它被剪切掉。所以解决方案是创建一个剪切矩形,其边缘位于元素的 `border-box` 的这三个方向的外部。
右侧的距离 ( `dr` ) 在折叠情况下 ( 复选框未选中,`--i: 0` ) 为整个条形图宽度 `$bar-w` 减去按钮半径 `$btn-r`,在展开情况下 ( 复选框选中,`--i: 1` ) 为 `0`。
$out-d: -3em;
[id='search-bar'] {
/* same as before */
clip-path: inset($out-d calc(var(--j)*#{$bar-w - $btn-r}) $out-d $out-d);
}
现在我们有一个搜索栏和按钮组件,它可以在点击按钮时展开和折叠。
查看 thebabydino 在 CodePen 上创建的 Pen ( @thebabydino )。
由于我们不希望在两种状态之间有突然的变化,我们使用 `transition`
[id='search-btn'] {
/* same as before */
~ * {
/* same as before */
transition: .65s;
}
}
我们还希望按钮的 `background` 在折叠状态 ( 复选框未选中,`--i: 0` ) 为绿色,在展开状态 ( 复选框选中,`--i: 1` ) 为粉色。为此,我们使用与之前相同的技术
[for='search-btn'] {
/* same as before */
$c0: #d9eb52; // green for collapsed state
$c1: #dd1d6a; // pink for expanded state
$h0: round(hue($c0)/1deg);
$s0: round(saturation($c0));
$l0: round(lightness($c0));
$h1: round(hue($c1)/1deg);
$s1: round(saturation($c1));
$l1: round(lightness($c1));
background: hsl(calc(var(--j)*#{$h0} + var(--i)*#{$h1}),
calc(var(--j)*#{$s0} + var(--i)*#{$s1}),
calc(var(--j)*#{$l0} + var(--i)*#{$l1}));
}
现在我们正在取得进展!
查看 thebabydino 在 CodePen 上创建的 Pen ( @thebabydino )。
我们仍然需要做的是创建图标,它在折叠状态下转变为放大镜,在展开状态下转变为“x”,以指示关闭操作。我们使用 `:before` 和 `:after` 伪元素来完成此操作。我们首先决定放大镜的直径,以及图标线条宽度占此直径的比例。
$ico-d: .5*$bar-h;
$ico-f: .125;
$ico-w: $ico-f*$ico-d;
我们将这两个伪元素绝对定位在按钮的中间,并考虑它们的尺寸。然后我们使它们 `inherit` 它们的父元素的 `transition`。我们给 `:before` 一个 `background`,因为这将是我们放大镜的把手,使 `:after` 通过 `border-radius` 变圆,并给它一个内嵌 `box-shadow`。
[for='search-btn'] {
/* same as before */
&:before, &:after {
position: absolute;
top: 50%; left: 50%;
margin: -.5*$ico-d;
width: $ico-d;
height: $ico-d;
transition: inherit;
content: ''
}
&:before {
margin-top: -.4*$ico-w;
height: $ico-w;
background: currentColor
}
&:after {
border-radius: 50%;
box-shadow: 0 0 0 $ico-w currentColor
}
}
现在我们可以在按钮上看到放大镜组件
查看 thebabydino 在 CodePen 上创建的 Pen ( @thebabydino )。
为了使我们的图标更像放大镜,我们将它的两个组件向外 `translate` 放大镜直径的四分之一。这意味着将把手向右移动,在 x 轴的正方向移动 `0.25*$ico-d`,并将主要部分向左移动,在 x 轴的负方向移动相同的 `0.25*$ico-d`。
我们还沿着 x 轴 ( 这意味着 `transform-origin` 为 `100%` ) 的右侧 `scale` 手柄 ( `:before` 伪元素 ) 的水平方向使其宽度减半。
我们只希望这在折叠状态 ( 复选框未选中,`--i` 为 `0`,因此 `--j` 为 `1` ) 下发生,所以我们将平移量乘以 `--j`,并使用 `--j` 来控制缩放比例
[for='search-btn'] {
/* same as before */
&:before {
/* same as before */
height: $ico-w;
transform:
/* collapsed: not checked, --i is 0, --j is 1
* translation amount is 1*.25*$d = .25*$d
* expanded: checked, --i is 1, --j is 0
* translation amount is 0*.25*$d = 0 */
translate(calc(var(--j)*#{.25*$ico-d}))
/* collapsed: not checked, --i is 0, --j is 1
* scaling factor is 1 - 1*.5 = 1 - .5 = .5
* expanded: checked, --i is 1, --j is 0
* scaling factor is 1 - 0*.5 = 1 - 0 = 1 */
scalex(calc(1 - var(--j)*.5))
}
&:after {
/* same as before */
transform: translate(calc(var(--j)*#{-.25*$ico-d}))
}
}
现在我们在折叠状态下有了放大镜图标
查看 thebabydino 在 CodePen 上创建的 Pen ( @thebabydino )。
由于我们希望两个图标组件都旋转 `45deg`,我们在按钮本身添加了这个旋转
[for='search-btn'] {
/* same as before */
transform: translate(calc(var(--j)*#{-$x})) rotate(45deg);
}
现在我们有了折叠状态下的外观
查看 thebabydino 在 CodePen 上创建的 Pen ( @thebabydino )。
这仍然留下了展开状态,我们需要将圆形的 `:after` 伪元素变成一条线。我们通过沿 x 轴缩放它并将其 `border-radius` 从 `50%` 降低到 `0%` 来实现这一点。我们使用的缩放比例是我们要获得的线条宽度 `$ico-w` 与它在折叠状态下形成的圆形的直径 `$ico-d` 之间的比例。我们称这个比例为 `$ico-f`。
由于我们只希望在展开状态 ( 当复选框选中且 `--i` 为 `1` 时 ) 进行此操作,我们将缩放比例和 `border-radius` 都与 `--i` 和 `--j` 相关联
$ico-d: .5*$bar-h;
$ico-f: .125;
$ico-w: $ico-f*$ico-d;
[for='search-btn'] {
/* same as before */
&:after{
/* same as before */
/* collapsed: not checked, --i is 0, --j is 1
* border-radius is 1*50% = 50%
* expanded: checked, --i is 1, --j is 0
* border-radius is 0*50% = 0 */
border-radius: calc(var(--j)*50%);
transform:
translate(calc(var(--j)*#{-.25*$ico-d}))
/* collapsed: not checked, --i is 0, --j is 1
* scaling factor is 1 + 0*$ico-f = 1
* expanded: checked, --i is 1, --j is 0
* scaling factor is 0 + 1*$ico-f = $ico-f */
scalex(calc(1 - var(--j)*.5))
}
}
查看 thebabydino 在 CodePen 上创建的 Pen ( @thebabydino )。
嗯,差不多,但还不是完全一样。缩放还沿 x 轴缩小了我们的内嵌 `box-shadow`,所以让我们用第二个内嵌阴影来修复它,我们只在展开状态 ( 当复选框选中且 `--i` 为 `1` 时 ) 获取它,因此它的扩展和 alpha 取决于 `--i`
$ico-d: .5*$bar-h;
$ico-f: .125;
$ico-w: $ico-f*$ico-d;
[for='search-btn'] {
/* same as before */
--hsl: 0, 0%, 0%;
color: HSL(var(--hsl));
&:after{
/* same as before */
box-shadow:
inset 0 0 0 $ico-w currentcolor,
/* collapsed: not checked, --i is 0, --j is 1
* spread radius is 0*.5*$ico-d = 0
* alpha is 0
* expanded: checked, --i is 1, --j is 0
* spread radius is 1*.5*$ico-d = .5*$ico-d
* alpha is 1 */
inset 0 0 0 calc(var(--i)*#{.5*$ico-d}) HSLA(var(--hsl), var(--i))
}
}
这给了我们最终的结果!
查看 thebabydino 在 CodePen 上创建的 Pen ( @thebabydino )。
更多快速示例
以下是使用相同技术的几个更多演示。我们不会从头开始构建这些,我们只会介绍它们背后的基本思路。
响应式横幅
在这种情况下,我们的实际元素是前面的较小的矩形,而后面的数字方块和较大的矩形则是用 `:before` 和 `:after` 伪元素创建的。
数字方块的背景是单独的,并使用一个停止列表变量 `--slist` 设置,该变量对于每个项目都不同。
<p style='--slist: #51a9ad, #438c92'><!-- 1st paragraph text --></p>
<p style='--slist: #ebb134, #c2912a'><!-- 2nd paragraph text --></p>
<p style='--slist: #db4453, #a8343f'><!-- 3rd paragraph text --></p>
<p style='--slist: #7eb138, #6d982d'><!-- 4th paragraph text --></p>
影响横幅样式的事物是奇偶性和我们是在宽、正常还是窄的情况下。这些为我们提供了切换变量
html {
--narr: 0;
--comp: calc(1 - var(--narr));
--wide: 1;
@media (max-width: 36em) { --wide: 0 }
@media (max-width: 20em) { --narr: 1 }
}
p {
--parity: 0;
&:nth-child(2n) { --parity: 1 }
}
数字方块是绝对定位的,它们的位置取决于奇偶性。如果 `--parity` 开关关闭 ( `0` ),则它们位于左边。如果它打开 ( `1` ),则它们位于右边。
`left: 0%` 值将数字方块的左边缘与它的父元素的左边缘对齐,而 `left: 100%` 值将它的左边缘与父元素的右边缘对齐。
为了使数字方块的右边缘与它的父元素的右边缘对齐,我们需要从之前的 `100%` 值中减去它自身的宽度。( 请记住,在偏移量的情况下,`%` 值是相对于父元素的尺寸 )。
left: calc(var(--parity)*(100% - #{$num-d}))
…其中 `$num-d` 是数字方块的大小。
在宽屏情况下,我们也会将编号向外推 1em
- 这意味着从奇数项(--parity
开关关闭)的偏移量中减去 1em
,并从偶数项(--parity
开关打开)的偏移量中加上 1em
。
现在问题是... 我们如何改变符号?最简单的方法是使用 -1
的幂。可惜的是,我们并没有在 CSS 中提供幂函数(或幂运算符),尽管在这种情况下它会非常有用。
/*
* for --parity: 0, we have pow(-1, 0) = +1
* for --parity: 1, we have pow(-1, 1) = -1
*/
pow(-1, var(--parity))
这意味着我们必须用现有的工具(加、减、乘、除)来实现它,这导致了一个奇怪的小公式... 但嘿,它确实有效!
/*
* for --parity: 0, we have 1 - 2*0 = 1 - 0 = +1
* for --parity: 1, we have 1 - 2*1 = 1 - 2 = -1
*/
--sign: calc(1 - 2*var(--parity))
这样,我们最终用于左侧偏移量的公式,考虑到奇偶性和是否处于宽屏状态(--wide: 1
)或非宽屏状态(--wide: 0
),变为
left: calc(var(--parity)*(100% - #{$num-d}) - var(--wide)*var(--sign)*1em)
我们还用这些变量和 max-width
来控制段落的 width
,因为我们希望它有一个上限,并且只有在窄屏状态下(--narr: 1
)才能完全覆盖其父元素的水平方向。
width: calc(var(--comp)*80% + var(--narr)*100%);
max-width: 35em;
font-size
也取决于我们是否处于窄屏状态(--narr: 1
)或非窄屏状态(--narr: 0
)。
calc(.5rem + var(--comp)*.5rem + var(--narr)*2vw)
... 以及 :after
伪元素(后面的较大矩形)的水平偏移量,因为它们在窄屏状态下(--narr: 1
)为 0
,而在非窄屏状态下(--narr: 0
)则为非零偏移量 $off-x
。
right: calc(var(--comp)*#{$off-x});
left: calc(var(--comp)*#{$off-x});
悬停和聚焦效果
此效果通过链接元素及其两个伪元素在 :hover
和 :focus
状态下沿对角线滑动来创建。链接的尺寸是固定的,其伪元素的尺寸也是固定的,设置为其父元素 $btn-d
的对角线(计算为由宽度和高度形成的直角三角形中的斜边)水平方向,以及父元素 height
垂直方向。
:before
的位置使其左下角与父元素的左下角重合,而 :after
的位置使其右上角与父元素的右上角重合。由于两者都应与父元素具有相同的 height
,因此垂直位置通过设置 top: 0
和 bottom: 0
来解决。水平位置使用 --i
作为在两个伪元素之间改变值的开关变量,以及 --j
(其补充,calc(1 - var(--i))
)来处理,与之前的示例相同。
left: calc(var(--j)*(100% - #{$btn-d}))
我们设置 :before
的 transform-origin
为其左下角(0% 100%
),:after
的 transform-origin
为其右上角(100% 0%
),再次使用开关 --i
及其补充 --j
。
transform-origin: calc(var(--j)*100%) calc(var(--i)*100%)
我们将两个伪元素旋转到对角线和水平线 $btn-a
之间的角度(也从高度和宽度形成的三角形计算而来,作为两个之间的反正切)。通过这种旋转,水平边沿沿着对角线相遇。
然后我们将它们向外移动它们自身的宽度。这意味着我们将对两者使用不同的符号,同样取决于在 :before
和 :after
之间改变值的开关变量,就像之前使用横幅的示例一样。
transform: rotate($btn-a) translate(calc((1 - 2*var(--i))*100%))
在 :hover
和 :focus
状态下,这种平移需要回到 0
。这意味着我们将上面的平移量乘以开关变量 --p
的补充 --q
,它在正常状态下为 0
,在 :hover
或 :focus
状态下为 1
。
transform: rotate($btn-a) translate(calc(var(--q)*(1 - 2*var(--i))*100%))
为了使伪元素在鼠标移出或失去焦点时以其他方式滑出(而不是按原路返回),我们将开关变量 --i
设置为 :before
的 --p
值,设置为 :after
的 --q
值,反转平移的符号,并确保我们只转换 transform
属性。
响应式信息图表
在这种情况下,每个项目(article
元素)都有一个三行两列的网格,在宽屏情况下,第三行折叠,在窄屏情况下,第二列折叠。在宽屏情况下,列的宽度取决于奇偶性。在窄屏情况下,第一列跨越元素的整个内容框,第二列的宽度为 0
。我们还在列之间留有间隙,但只有在宽屏情况下。
// formulas for the columns in the wide screen case, where
// $col-a-wide is for second level heading + paragraph
// $col-b-wide is for the first level heading
$col-1-wide: calc(var(--q)*#{$col-a-wide} + var(--p)*#{$col-b-wide});
$col-2-wide: calc(var(--q)*#{$col-b-wide} + var(--p)*#{$col-a-wide});
// formulas for the general case, combining the wide and normal scenarios
$row-1: calc(var(--i)*#{$row-1-wide} + var(--j)*#{$row-1-norm});
$row-2: calc(var(--i)*#{$row-2-wide} + var(--j)*#{$row-2-norm});
$row-3: minmax(0, auto);
$col-1: calc(var(--i)*#{$col-1-wide} + var(--j)*#{$col-1-norm});
$col-2: calc(var(--i)*#{$col-2-wide});
$art-g: calc(var(--i)*#{$art-g-wide});
html {
--i: var(--wide, 1); // 1 in the wide screen case
--j: calc(1 - var(--i));
@media (max-width: $art-w-wide + 2rem) { --wide: 0 }
}
article {
--p: var(--parity, 0);
--q: calc(1 - var(--p));
--s: calc(1 - 2*var(--p));
display: grid;
grid-template: #{$row-1} #{$row-2} #{$row-3}/ #{$col-1} #{$col-2};
grid-gap: 0 $art-g;
grid-auto-flow: column dense;
&:nth-child(2n) { --parity: 1 }
}
由于我们设置了 grid-auto-flow: column dense
,我们可以只设置一级标题来覆盖整个列(奇数项的第二列,偶数项的第一列),并在宽屏情况下让二级标题和段落文本填充第一个可用的空单元格。
// wide case, odd items: --i is 1, --p is 0, --q is 1
// we're on column 1 + 1*1 = 2
// wide case, even items: --i is 1, --p is 1, --q is 0
// we're on column 1 + 1*0 = 1
// narrow case: --i is 0, so var(--i)*var(--q) is 0 and we're on column 1 + 0 = 1
grid-column: calc(1 + var(--i)*var(--q));
// always start from the first row
// span 1 + 2*1 = 3 rows in the wide screen case (--i: 1)
// span 1 + 2*0 = 1 row otherwise (--i: 0)
grid-row: 1/ span calc(1 + 2*var(--i));
对于每个项目,其他一些属性也取决于我们是否处于宽屏状态。
垂直 margin
、垂直和水平 padding
值、box-shadow
偏移量和模糊在宽屏情况下都更大。
$art-mv: calc(var(--i)*#{$art-mv-wide} + var(--j)*#{$art-mv-norm});
$art-pv: calc(var(--i)*#{$art-pv-wide} + var(--j)*#{$art-p-norm});
$art-ph: calc(var(--i)*#{$art-ph-wide} + var(--j)*#{$art-p-norm});
$art-sh: calc(var(--i)*#{$art-sh-wide} + var(--j)*#{$art-sh-norm});
article {
/* other styles */
margin: $art-mv auto;
padding: $art-pv $art-ph;
box-shadow: $art-sh $art-sh calc(3*#{$art-sh}) rgba(#000, .5);
}
我们在宽屏情况下有非零 border-width
和 border-radius
。
$art-b: calc(var(--i)*#{$art-b-wide});
$art-r: calc(var(--i)*#{$art-r-wide});
article {
/* other styles */
border: solid $art-b transparent;
border-radius: $art-r;
}
在宽屏情况下,我们限制项目的 width
,但在其他情况下将其设置为 100%
。
$art-w: calc(var(--i)*#{$art-w-wide} + var(--j)*#{$art-w-norm});
article {
/* other styles */
width: $art-w;
}
padding-box
梯度的方向也会根据奇偶性变化。
background:
linear-gradient(calc(var(--s)*90deg), #e6e6e6, #ececec) padding-box,
linear-gradient(to right bottom, #fff, #c8c8c8) border-box;
类似地,margin
、border-width
、padding
、width
、border-radius
、background
梯度方向、标题和段落文本的 font-size
或 line-height
也取决于我们是否处于宽屏状态(以及在一级标题的 border-radius
或 background
梯度方向的情况下,还取决于奇偶性)。
我简直不敢相信还没有人评论。这真的太棒了。当然,很复杂,但终于有一些关于为什么我在浏览器而不是 SASS 中使用变量的可靠示例了。感谢您抽出时间!
这真的很有趣,我过去几天一直在玩弄 CSS 变量!可惜浏览器支持还不太完善,但令人兴奋!
“为什么我在浏览器而不是 SASS 中使用变量。”
值得一提的是,CSS 变量也可以从 JavaScript 中读写。因此,您可以从代码中应用很多有趣的东西。如果您开始使用 Web Components / 自定义元素,CSS 变量(以及来自 JS 的操作)是跨越 Shadow DOM 屏障的好方法。