CSS变量在前端复杂布局和交互上的探索

2,005 阅读15分钟


写在前面:

本文将和各位老铁一起探索如何使用CSS变量来降低复杂布局和交互的代码编写难度,并使其更易于维护。这里即将分享两篇该系列的文章,本篇是分享css变量的探索应用的各种用例。(ps:看懂和理解本文需要有一定的前端基础)

进入文章前,我们先看下图,一张网站常用的响应式信息图表布局图,当然实现的方式很多,如果我告诉您一个CSS声明会在以下图像中对宽屏情况(左测)和第二个(右测)之间产生差异,用一个CSS声明实现在宽屏幕的情况下对奇数项和偶数项进行区分,从而来实现下面效果,你会不会觉得有点惊喜?

On the left, a screenshot of the wide screen scenario. Each item is limited in width and its components are arranged on a 2D 2x2 grid, with the first level heading occupying an entire column, either the one on the right (for odd items) or the one on the left (for even items). The second level heading and the actual text occupy the other column. The shape of the first level heading also varies depending on the parity — it has the top left and the bottom right corners rounded for the odd items and the other two corners rounded for the even items. On the right, a screenshot of the narrower scenario. Each item spans the full viewport width and its components are placed vertically, one under another — first level heading, second level heading below and, finally, the actual text.

或者仅仅一个CSS声明就能区别下面的压缩和扩展的情况?

Animated gif. Shows a green button with a magnifier icon. Clicking this button makes it slide right and its background to turn red while a text search field slides out of it to the left and the magnifier morphs into a close (crossmark) icon.

有点意思吧,接下来,我们就一起探索CSS变量在复杂布局和交互的一些小运用。关于CSS变量是什么以及如何开始使用它们的文章已经很多了,所以我们不会在这里深入讨论。我们将直接深入了解CSS变量为什么对实现这些情况和其他情况有用,然后我们将进一步详细解释如何实现各种情况。我们将从头编写一个实际示例,一步一步地编写,最后,您将通过使用相同技术的更多演示获得一些引人注目的东西。所以让我们开始吧!(ps:本文样式都是scss编写)《DRY Switching with CSS Variables: The Difference of One Declaration》BY ANA TUDOR ONDECEMBER 5, 2018(注:原文)


为什么CSS变量很有用

对我而言,CSS变量的最大优点是它们以逻辑,数学和轻松的方式为事物的样式打开了大门。看下面这个阴阳图,其实是使用loader元素的两个伪元素创建两个半部分

Animated gif. The yin and yang symbol is rotating while its two lobes alternate increasing and decreasing in size - whenever one is increasing, it squishes the other one down.

旋转☯符号,两个半的大小逐渐增大和减小。我们使用相同的backgroundborder-colortransform-originanimation-delay值两半。这些值都取决于--i最初设置为0两半(伪元素)的开关变量,但随后我们将其更改1为后半部分(:after伪元素),从而动态修改所有这些属性的计算值。

如果没有CSS变量,我们将不得不再次在:after伪元素上设置所有这些属性(border-color、transform-origin、background、anima -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

您可以看到以下所示的概念:

GIF动画。 显示如何将开关值从0更改为1更改两个框的旋转。 当开关关闭时(其值为0),第一个框旋转到30度,当开关打开时,它的第一个框不旋转或旋转到0度(其值为1)。 这意味着我们的旋转值为calc((1  -  var( -  i))* 30deg),其中 -  i是开关值。 当开关关闭时(其值为0),第二个盒子不旋转或旋转到0度,当开关打开时,它旋转到30度(其值为1)。 这意味着我们的旋转值为calc(var( -  i)* 30deg), -  i是开关值。

在零和非零值之间切换(现场演示,由于calc()不处理角度值而没有Edge支持)
对于装载机的特定情况下,我们使用HSL值border-colorbackground-color。HSL代表色调,饱和度,亮度,并且可以借助于双锥体(其由粘合在一起的基部的两个锥体组成)在视觉上最佳地表示。


两个锥体的基部在中间粘在一起,一个顶点朝下,一个朝上。 色调是循环的,分布在双锥的中心(垂直)轴周围。 饱和轴从中心轴朝向双锥体表面水平移动 - 它在轴上为0%,在表面上为100%。 亮度轴从黑色顶点垂直移动到白色顶点 - 在黑色顶点处为0%,在白色顶点处为100%。

色调围绕着双色调,相当于360°在两种情况下都给我们一个红色

显示红色为0°(相当于360Â°ï¼Œå› ä¸ºè‰²è°ƒæ˜¯å¾ªçŽ¯çš„ï¼‰ï¼Œé»„è‰²ä¸º60°,石灰为120°,青色为180°,蓝色为240°,品红色为300° 。


饱和度从0%双锥的垂直轴到100%双锥面上。当饱和度0%(在双锥的垂直轴上)时,色调不再重要;对于同一水平面上的所有色调,我们得到完全相同的灰色。“相同水平面”是指具有相同的亮度,其沿垂直轴双锥增加,从去0%在黑色双圆锥体的顶点,以100%在白色双圆锥体的顶点。当亮度是0%或者100%,色调和饱和度都不再重要时 - 我们总是得到黑色亮度值0%和白色亮度值100%。因为我们只需要黑色和白色我们的☯符号,色调和饱和度是不相关的,所以我们清除它们,然后之间进行切换黑色,并白色通过切换之间的亮度0%100%

.yin-yang {
  &:before, &:after {
    --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 }
}


但是,如果我们想要在开关关闭(--i: 0)时具有非零值而在开关打开时具有另一个不同的非零值(--i: 1)呢?

一、在两个非零值之间切换

假设我们希望一个元素在开关关闭时具有灰色background#ccc),在开关打开时--i: 0
具有橙色background#f90)(--i: 1)。我们要做的第一件事是从十六进制切换到更易于管理的格式,如rgb()hsl()。我们可以通过使用诸如Lea Verou的CSS Colors之类的工具或通过DevTools手动完成此操作。如果我们background在元素上有一个集合,我们可以通过Shift按住键来循环浏览格式,同时单击DevTools中值前面的正方形(或圆圈)。这适用于Chrome和Firefox,但它似乎不适用于Edge。

GIF动画。 演示如何通过DevToolså¾ªçŽ¯æ ¼å¼åŒ–ï¼ˆåå…­è¿›åˆ¶/ RGB / HSL)。 在Chrome和Firefox中,我们通过按住Shift键并单击<color>值前面的方框或圆圈来完成此操作。

更妙的是,如果我们使用无礼的话,我们可以提取成分red()/green()/blue()hue()/

saturation()/lightness()函数。虽然rgb()可能是更为人所知的格式,但我更倾向于hsl()因为我发现它更直观,而且通过查看代码,我更容易了解视觉效果。因此,我们使用以下函数提取

hsl()两个值的等价物的三个组成部分($c0: #ccc当开关关闭$c1: #f90时和开关打开时):

$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()函数内部hue的单位值,因为Sass将其视为Sass hsl()函数,它被编译为hsl()具有无单位色调的CSS 函数。但是在这里,我们内部有一个动态CSS变量,因此Sass将此函数视为CSShsl() 没有编译成其他任何东西的函数,因此,如果hue有一个单元,则不会从生成的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)

在这里,我们记--j的互补值--i(当--i是0,--j是1,当--i是1,--j是0)。

GIF动画。 显示如何将开关值从0更改为1更改框的背景。 当开关关闭(其值为零)和橙色(色调$ h1,饱和度$ s1和亮度$ l1)时,背景为灰色(色调$ h0,饱和度$ s0和亮度$ l0)打开(其值为1)。 这意味着我们有一个色调值calc(var( -  j)*#{$ h0} + var( -  i)*#{$ h1}),一个饱和度值为calc(var( -  j)* #{$ s0} + var( -  i)*#{$ s1})和亮度值calc(var( -  j)*#{$ l0} + var( -  i)*#{$ l1 })),其中--i是switch变量。

上述公式适用于在任意两个HSL值之间切换。但是,在这种特殊情况下,我们可以简化它,因为当开关关闭时我们有一个纯灰色(--i: 0)。

 考虑到RGB模型,纯灰度值具有相等的红色,绿色和蓝色值。 当考虑HSL模型时,色调是无关紧要的(我们的灰色看起来对于所有色调都是相同的),饱和度总是0%只有亮度很重要,决定了我们的灰色是多么亮或暗。 在这种情况下,我们可以始终保持非灰色值的色调(我们对“on”情况所具有的色调$h1)。

 由于任何灰度值(我们对“关闭”情况所具有的)的饱和度 $s0始终是0%,将其乘以0或者1总是给出我们0%。因此,考虑到var(--j)*#{$s0}我们的公式中的术语总是如此0%,我们可以放弃它,我们的饱和公式减少到“on”情况$s1和switch变量饱和之间的乘积--i。 这使得轻盈成为我们仍然需要应用完整配方的唯一组成部分

--j: calc(1 - var(--i));
background: hsl($h1, 
                calc(var(--i)*#{$s1}), 
                calc(var(--j)*#{$l0} + var(--i)*#{d1l}))

以上内容可在此演示中进行测试。 类似地,假设我们想要font-size一些文本2rem在我们的开关关闭(--i: 0)和10vw开关打开(--i: 1)时。应用相同的方法,我们有:

font-size: calc((1 - var(--i))*2rem + var(--i)*10vw)

GIF动画。 显示如何将开关值从0更改为1更改字体大小。


触发切换

一、基于元素的切换

这意味着某些元件和其他元件的开关关闭。例如,这可以通过奇偶校验来确定。假设我们希望所有偶数元素都旋转并且具有橙色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 }
}

截图。 连续显示一组正方形,偶数正方形旋转并具有橙色背景而不是初始灰色背景。 这是通过使变换和背景属性都依赖于切换变量来实现的 - å®ƒéšç€å¥‡å¶æ ¡éªŒå˜åŒ–ï¼šå®ƒæœ€åˆä¸º0,但是我们将偶数项更改为1。

由项目奇偶校验触发的切换(实时演示,由于calc()不适用于角度值而在Edge中不能完全正常工作)

在奇偶校验的情况下,我们为每隔一个项目(: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任何hsl()具有亮度100%(色调和饱和度都无关紧要)的值,我们可以通过始终保持:focus/ :hover状态的色调和饱和度并且仅改变亮度来简化事物。

GIF动画。 显示白色链接,当悬停或聚焦时会增长并变为橙色。

由状态变化触发的切换(现场演示,由于功能calc()内部不支持的值,Edge中功能不全scale())

三、基于媒体查询的切换

另一种可能性是切换由媒体查询触发,例如,当方向改变时或从一个视口范围转到另一个视口范围时。 比方说,我们有white一个标题font-size的1rem最高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 }
}

从头开始编写一个更复杂的例子-搜索

我们在这里剖析的例子是本文开头展示的扩展搜索

GIF动画。 æ˜¾ç¤ºå¸¦æœ‰æ”¾å¤§é•œå›¾æ ‡çš„ç»¿è‰²æŒ‰é’®ã€‚ å•å‡»æ­¤æŒ‰é’®å¯ä½¿å…¶å‘å³æ»‘åŠ¨ï¼Œå…¶èƒŒæ™¯å˜ä¸ºçº¢è‰²ï¼Œè€Œæ–‡æœ¬æœç´¢å­—æ®µå‘å·¦æ»‘åŠ¨ï¼Œæ”¾å¤§é•œå˜ä¸ºå…³é—­ï¼ˆäº¤å‰æ ‡è®°ï¼‰å›¾æ ‡ã€‚

ps:从可用性的角度来看,在网站上设置这样的搜索框可能不是最好的主意,因为人们通常期望搜索框后面的按钮触发搜索,而不是关闭搜索栏,但它仍然是一个有趣的编码练习,这就是为什么我选择在这里剖析它。

首先,我的想法是仅使用表单元素来完成它。
因此,HTML结构如下所示:

<input id='search-btn' type='checkbox'/>
<label for='search-btn'>Show search bar</label>
<input id='search-bar' type='text' placeholder='Search...'/>

我们在这里做的最初是隐藏文本input,然后在选中复选框之前将其显示出来 - 让我们深入了解它是如何工作的! 首先,我们使用基本重置并flex在我们input和label元素的容器上设置布局。在我们的例子中,这个容器是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
}


我们把复选框label变成一个大的绿色圆形按钮,并使用一个大的负数值移动它的文本内容的视线text-indent和overflow: hidden。

$btn-d: 5em; /* 同上 */[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;
}


接下来,我们通过以下方式修改实际搜索栏:

  • 给它明确的维度
  • 提供background正常状态
  • background为其聚焦状态定义不同的光晕
  • 使用border-radius相当于其一半的左侧角落height
  • 清理占位符一点

$btn-d: 5em;
$bar-w: 4*$btn-d;
$bar-h: .65*$btn-d;
$bar-r: .5*$bar-h;
$bar-c: #ffeacc;/* 同上 */

[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;
  }
}



此时,搜索栏的右边缘与按钮的左边缘重合。但是,我们想要一些重叠 - 假设重叠使得搜索栏的右边缘与按钮的垂直中线重合。鉴于我们align-items: center在容器上有一个flexbox布局(body在我们的例子中),由我们的两个项目组成的组件(条和按钮)保持水平中间对齐,即使我们设置margin一个或另一个或在这两个项目之间。(在最左边的项目的左侧或最右边的项目的右侧是一个不同的故事,但我们现在不会进入那个。)

æ’å›¾æ˜¾ç¤ºæ¡å½¢åŠ ä¸ŠæŒ‰é’®ç»„ä»¶å¤„äºŽåˆå§‹çŠ¶æ€ï¼ˆæ¡å½¢å›¾çš„å³è¾¹ç¼˜ä¸ŽæŒ‰é’®çš„å·¦è¾¹ç¼˜é‡åˆï¼‰ä¸Žé‡å çŠ¶æ€ï¼ˆæ¡å½¢å›¾çš„å³è¾¹ç¼˜ä¸ŽæŒ‰é’®çš„åž‚ç›´ä¸­çº¿é‡åˆï¼‰ã€‚ 在这两种情况下,组件都是中间对齐的。

这是.5*$btn-d半个按钮直径的重叠,相当于按钮的半径。我们margin-right在栏上将其设为负数。我们还调整padding条形图的右侧,以便我们补偿重叠:

$btn-d: 5em;
$btn-r: .5*$btn-d;

/* 同上 */

[id='search-bar'] {
  /* 同上 */
  margin-right: -$btn-r;
  padding: 0 calc(#{$btn-r} + 1em) 0 1em;
}


除了条形按照DOM顺序中的按钮,所以它放在它的顶部,当我们真正想要按钮在顶部。
幸运的是,这有一个简单的解决方案(至少现在 - 以后还不够,但让我们一次处理一个问题)。

[for='search-btn'] {
  /* 同上 */
  position: relative;
}


在这种状态下,条形和按钮组件的总宽度是条形宽度$bar-w加上按钮的半径$btn-r(按钮直径的一半$btn-d),因为按钮的一半重叠。在折叠状态下,组件的总宽度就是按钮直径$btn-d。

æ’å›¾æ˜¾ç¤ºå¤„äºŽå±•å¼€çŠ¶æ€çš„æ¡å½¢åŠ æŒ‰é’®ç»„ä»¶ï¼ˆæ¡å½¢å›¾çš„å³è¾¹ç¼˜ä¸ŽæŒ‰é’®çš„åž‚ç›´ä¸­çº¿é‡åˆï¼‰å¹¶å¤„äºŽæŠ˜å çŠ¶æ€ï¼ˆæ¡å½¢æŠ˜å ï¼Œç»„ä»¶ç¼©å°ä¸ºæŒ‰é’®ï¼‰ã€‚ 在这两种情况下,组件都是中间对齐的。

由于我们希望在从展开状态到折叠状态时保持相同的中心轴,我们需要将按钮向左移动扩展状态(.5*($bar-w + $btn-r))减去按钮半径($btn-r)的组件宽度的一半。 我们称之为这种转变$x,我们在按钮上使用减号(因为我们将按钮向左移动,左边是x轴的负方向)。由于我们希望条形图折叠到按钮中,我们$x在它上面设置相同的偏移,但是在正方向上(因为我们将条形图移动到右边的 x轴)。 当未选中复选框时,我们处于折叠状态,而当未选中复选框时,我们处于展开状态。这意味着transform当没有选中复选框时,我们的栏和按钮会被CSS移动,并且我们当前将它们置于其中(没有transform)。 为了做到这一点,我们--i在复选框后面的元素上设置了一个变量- 按钮(使用label复选框创建)和搜索栏。此变量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'] {
  /*同之前 */
  transform: translate(calc(var(--j)*#{-$x}));
}

[id='search-bar'] {
  /*同之前 */
  transform: translate(calc(var(--j)*#{$x}));
}

我们现在有互动的东西!
单击该按钮可切换复选框状态(因为该按钮已使用label复选框创建)。


除了现在按钮有点难以点击,因为它再次位于文本输入下(因为我们在条上设置了transform
并建立了堆叠上下文)。修复非常简单 - 我们需要z-index在按钮上添加一个按钮,然后将其移动到条形图上方。

但是我们还有另一个更大的问题:我们可以看到右侧按钮下方的栏杆。为了解决这个问题,我们在栏上设置clip-path了一个inset()值。这指定了一个剪切矩形,借助于元素顶部,右侧,底部和左侧边缘的距离border-box。剪切矩形之外的所有内容都会被剪切掉,只显示内部的内容

插图显示了inset()函数的四个值代表什么。 第一个是剪切矩形的上边缘相对于边框的上边缘的偏移。 第二个是剪切矩形的右边缘相对于边框的右边缘的偏移。 第三个是剪切矩形的下边缘相对于边框的下边缘的偏移。 第四个是剪切矩形的左边缘相对于边框的左边缘的偏移。 外面的一切

在上图中,每个距离都从边框的边缘向内移动。在这种情况下,他们是积极的。但它们也可以向外移动,在这种情况下它们是负的,并且剪切矩形的相应边缘在元素之外border-box。 起初,您可能认为我们没有理由这样做,但在我们的特定情况下,我们会这样做! 我们希望从top(dt),bottom(db)和left(dl)的距离是负的,并且足够大以包含在该状态中box-shadow的元素外部延伸的border-box距离,:focus因为我们不希望它被剪切掉。所以解决方案是创建一个剪切矩形,边缘在元素之外border-box在这三个方向。 与右边的距离(dr)是折叠情况下的整个条宽$bar-w减去按钮半径$btn-r(未选中复选框--i: 0),并且0在展开的情况下(选中复选框--i: 1)。

$out-d: -3em;

[id='search-bar'] {
  /* 同之前 */
  clip-path: inset($out-d calc(var(--j)*#{$bar-w - $btn-r}) $out-d $out-d);
}

我们现在有一个搜索栏和按钮组件,在单击按钮时会展开和折叠。

由于我们不希望两个便之间发生突然变化,我们使用transition

[id='search-btn'] {
  /* 同之前 */

  ~ * {
    /* same as before */
    transition: .65s;
  }
}

我们还希望我们的按钮background在折叠的情况下为绿色(未选中复选框--i: 0),在展开的情况下为粉红色(选中复选框--i: 1)。为此,我们使用与以前相同的技术:


[for='search-btn'] {
  /* 同之前 */
  $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}));
}

看看效果


我们仍然需要做的是创建一个图标,该图标在折叠状态的放大镜和展开状态的“x”之间变形,以指示关闭动作。我们使用:before和:after伪元素执行此操作。我们首先确定放大镜的直径以及图标线宽度所代表的直径。

$ico-d: .5*$bar-h;
$ico-f: .125;
$ico-w: $ico-f*$ico-d;

我们绝对将伪元素放在按钮中间,并考虑其尺寸。然后我们把它们变成inherit父母的transition。我们给:beforea background,因为这将是我们的放大镜的把手,使它成为:after圆形border-radius并给它一个插图box-shadow。

[for='search-btn'] {
  /* 同之前 */	
  &: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
  } 
}

我们现在可以在按钮上看到放大镜组件,为了使我们的图标看起来更像放大镜,我们的translate两个组件都向外放大了放大镜直径的四分之一。这意味着所述手柄平移向右,在正方向X通过轴.25*$ico-d与所述主要部分的左侧,在负方向X以相同的轴线.25*$ico-d。 我们还scale手柄(该:before伪元素)水平至其一半width相对于其右边缘(这意味着一个transform-origin的100%沿X轴)。 我们只希望在折叠状态下发生这种情况(复选框未选中,--i是0,因此--j是1),因此我们将转换量乘以--j并用于--j调整比例因子:

[for='search-btn'] {
  /* 同之前  */

  &:before {
    /* 同之前  */
    height: $ico-w;
    transform: 
      translate(calc(var(--j)*#{.25*$ico-d})) 
      scalex(calc(1 - var(--j)*.5))
  }
  &:after {
    /* same as before */
    transform: translate(calc(var(--j)*#{-.25*$ico-d}))
  } 
}

我们现在处于折叠状态的放大镜图标:


由于我们希望旋转两个图标组件45deg,我们在按钮本身上添加此旋转:

[for='search-btn'] {
  /* 同之前  */
  transform: translate(calc(var(--j)*#{-$x})) rotate(45deg);
}

这仍然会离开扩展状态,我们需要将圆形: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'] {
  /* 同之前  */	
  &:after{
    /* 同之前  */
    border-radius: calc(var(--j)*50%);
    transform: 
      translate(calc(var(--j)*#{-.25*$ico-d})) 
      scalex(calc(1 - var(--j)*.5))
  }
}


嗯,差不多,但并不完全。缩放也缩水了插图box-shadow沿X轴,所以我们进行了修复与第二插图影子,我们(当复选框被选中,并在扩展状态下只能得到--i是1),因此,它的传播和α取决于--i:

$ico-d: .5*$bar-h;
$ico-f: .125;
$ico-w: $ico-f*$ico-d;

[for='search-btn'] {
  /* 同之前 */
  --hsl: 0, 0%, 0%;
  color: HSL(var(--hsl));	
  &:after{
    /* 同之前 */
    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))
  }
}

这给了我们最终的结果!【本例完整源码】


实用案例

以下是一些使用相同技术的演示。我们不会从头开始构建这些 - 我们只会介绍它们背后的基本思想

一、响应横条

在左侧,是宽屏幕场景的屏幕截图。 在中间,是正常屏幕场景的屏幕截图。 右侧是窄屏幕场景的屏幕截图。

在这种情况下,我们的实际元素是前面较小的矩形,而后面的数字正方形和较大的矩形分别使用: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- 这意味着减去1em我们迄今为止对于奇数项目(--parity关闭开关)1em的偏移量,并添加到目前为止偶数项目的偏移量(--parity开启时) )。 现在问题是......我们如何切换标志?最简单的方法是使用的权力-1。遗憾的是,我们在CSS中没有幂函数(或幂函数运算符),即使它在这种情况下非常有用:

pow(-1, var(--parity))

这意味着我们必须使它与我们所拥有的(加法,减法,乘法和除法)一起工作,这导致了一个奇怪的小公式......但是,嘿,它有效!

--sign: calc(1 - 2*var(--parity))

left: calc(var(--parity)*(100% - #{$num-d}) - var(--wide)*var(--sign)*1em)

我们还width使用这些变量来控制段落,并且max-width我们希望它具有上限并且仅在窄case(--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伪元素(后面较大的矩形)的水平偏移也是如此,因为它们0在窄情况(--narr: 1)中,而非零偏移,$off-x否则(--narr: 0)

right: calc(var(--comp)*#{$off-x}); 
left: calc(var(--comp)*#{$off-x});


二、悬停并注重效果

GIF动画。 在悬停/焦点上显示红色对角滑动带,覆盖黑色文本下方的白色按钮。 在mouseout / blur上,乐队以另一种方式滑出,而不是他们进入的方式。

这个效果是通过一个链接元素和它的两个伪元素在对角: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}))

我们设定transform-origin的:before到它的左下角(0% 100%)和:after其右上角的(100% 0%),再次,与交换机的帮助--i和补充--j

transform-origin: calc(var(--j)*100%) calc(var(--i)*100%)

我们将两个伪元素旋转到对角线和水平线之间的角度 $btn-a(也是由高度和宽度形成的三角形计算出来的,作为两者之间比率的反正切)。通过这种旋转,水平边缘沿着对角线相交。 然后我们按照自己的宽度向外移动它们。这意味着我们将为两者中的每一个使用不同的符号,同样取决于在:before和之间改变值的switch变量,:after就像前面的横幅示例一样:

transform: rotate($btn-a) translate(calc((1 - 2*var(--i))*100%))

在:hover和:focus,这个切换必须回去0。这意味着我们通过互补乘以上面的平移量--q的开关变量--p这是0在正常状态下和1在:hover或:focus状态:

transform: rotate($btn-a) translate(calc(var(--q)*(1 - 2*var(--i))*100%))

为了使伪元素在鼠标移出或失焦时以相反的方式滑出(不回溯它们进入的方式),我们将switch变量设置--i为--pfor :before的值,并将值设置--q为for :after,反转转换的符号,确保只转换transform属性

三、响应信息图

在左侧,是宽屏幕场景的屏幕截图。 æˆ‘ä»¬æœ‰ä¸€ä¸ªä¸‰è¡Œï¼Œä¸¤åˆ—ç½‘æ ¼ï¼Œç¬¬ä¸‰è¡ŒæŠ˜å ï¼ˆé«˜åº¦ä¸ºé›¶ï¼‰ã€‚ ç¬¬ä¸€çº§æ ‡é¢˜å æ®å³ä¾§çš„åˆ—ï¼ˆå¯¹äºŽå¥‡æ•°é¡¹ï¼‰æˆ–å·¦ä¾§çš„åˆ—ï¼ˆå¯¹äºŽå¶æ•°é¡¹ï¼‰ã€‚ ç¬¬äºŒçº§æ ‡é¢˜ä½äºŽå¦ä¸€åˆ—å’Œç¬¬ä¸€è¡Œï¼Œè€Œæ®µè½æ–‡æœ¬ä½äºŽç¬¬äºŒè¡Œçš„ç¬¬äºŒçº§æ ‡é¢˜ä¸‹æ–¹ã€‚ 右侧是较窄场景的屏幕截图。 åœ¨è¿™ç§æƒ…å†µä¸‹ï¼Œç¬¬ä¸‰è¡Œçš„é«˜åº¦è¶³ä»¥é€‚åˆæ®µè½æ–‡æœ¬ï¼Œä½†ç¬¬äºŒåˆ—æ˜¯æŠ˜å çš„ã€‚ ç¬¬ä¸€çº§å’Œç¬¬äºŒçº§æ ‡é¢˜åˆ†åˆ«å æ®ç¬¬ä¸€å’Œç¬¬äºŒè¡Œã€‚

在这种情况下,我们为每个项目(article元素)都有一个三行,两列网格,第三行在宽屏幕场景中折叠,第二列在窄屏幕场景中折叠。在宽屏幕场景中,列的宽度取决于奇偶校验。在窄屏场景中,第一列跨越元素的整个内容框,第二列具有宽度0。我们在列之间也存在差距,但仅限于宽屏场景。【本例源码

$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});
$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); 
  --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,我们可以在宽屏幕情况下只设置第一级标题来覆盖整个列(第二个用于奇数项,第一个用于偶数项),并让第二个级别标题和段落文本填写第一个可用单元格。

grid-column: calc(1 + var(--i)*var(--q));
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 {
  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 {
  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 {
  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梯度方向,也在奇偶校验)。

看效果:


最后

本篇文章主要介绍了使用CSS变量来驱动布局和交互的切换的策略,其中值得注意的是:只适用于数值 - 长度,百分比,角度,持续时间,频率,无单位数值等,下篇文章继续分享前端复杂布局和交互上的探索新的“意淫”技巧。本篇检索和响应信息图的【源码地址】