使用 CSS 变量实现 DRY 状态切换:回退值和无效值

Avatar of Ana Tudor
Ana Tudor

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

这是关于 CSS 变量如何用于使复杂布局和交互的代码更易于编写和维护的两部分系列文章中的第二篇。第一篇 文章 阐述了此技术适用的各种用例。本文介绍了回退值和无效值的使用,将该技术扩展到非数值。

我们在本系列的 第一篇文章 中介绍的使用 CSS 变量驱动布局和交互切换的策略存在一个主要警告:它仅适用于数值——长度、百分比、角度、持续时间、频率、无单位数值等等。因此,知道可以使用单个 CSS 变量切换十多个属性的计算值,但随后需要显式切换诸如 flex-directiontext-align 等属性的非数值(例如,从 row 切换到 column,或从 left 切换到 right,反之亦然),这确实令人沮丧。

以下是一个示例,其中 text-align 属性取决于奇偶性,而 flex-direction 属性取决于我们是否在宽屏场景中查看前端。

Screenshot collage. On the left, we have the wide screen scenario, with four paragraphs as the four horizontal, offset based on parity slices of a disc. The slice numbering position is either to the right or left of the actual text content, depending on parity. The text alignment also depends on parity. In the middle, we have the normal screen case. The paragraphs are now full width rectangular elements. On the right, we have the narrow screen case. The paragraph numbering is always above the actual text content in this case.
截图拼贴。

我对此表示了抱怨,并收到了一个 非常有趣的建议,该建议利用了 CSS 变量的回退值和无效值。它很有趣,并为我们提供了新的内容,所以让我们从简要回顾一下什么是回退值和无效值开始吧!

回退值

CSS 变量的回退值是 var() 函数的第二个可选参数。例如,假设我们有一些 .box 元素,其背景设置为 --c 变量。

.box { background: var(--c, #ccc) }

如果我们没有在其他地方显式指定 --c 变量的值,则使用回退值 #ccc

现在假设其中一些框具有 .special 类。在这里,我们可以将 --c 指定为某种橙色。

.special { --c: #f90 }

这样,具有此 .special 类的框具有橙色的 background,而其他框使用浅灰色回退值。

查看 thebabydino 在 CodePen 上创建的 Pen@thebabydino)。

这里需要注意一些事项。

首先,回退值可以是另一个 CSS 变量,该变量本身可以具有 CSS 变量回退值,并且……我们可以通过这种方式陷入一个非常深的兔子洞!

background: var(--c, var(--c0, var(--c1, var(--c2, var(--c3, var(--c4, #ccc))))))

其次,逗号分隔列表是一个完全有效的回退值。实际上,在 var() 函数中第一个逗号之后指定的所有内容都构成回退值,如下面的示例所示。

background: linear-gradient(90deg, var(--stop-list, #ccc, #f90))

查看 thebabydino 在 CodePen 上创建的 Pen@thebabydino)。

最后但并非最不重要的一点是,我们可以为在不同位置使用的相同变量设置不同的回退值,如下面的示例所示。

$highlight: #f90;

a {
  border: solid 2px var(--c, #{rgba($highlight, 0)})
  color: var(--c, #ccc);
  
  &:hover, &:focus { --c: #{$highlight} }
}

查看 thebabydino 在 CodePen 上创建的 Pen@thebabydino)。

无效值

首先,我想澄清一下我的意思。“无效值”更短更容易记住,但它实际上指的是任何使声明在计算值时 无效 的值。

例如,考虑以下代码片段。

--c: 1em;
background: var(--c)

1em 是一个有效的长度值,但这不是 background-color 属性的有效值,因此此处此属性将采用其初始值(即 transparent)。

综合运用

假设我们有一堆段落,我们根据奇偶性更改 color 值的亮度以在 blackwhite 之间切换(如本系列上一篇文章中所述)。

p {
  --i: 0;
  /* for --i: 0 (odd), the lightness is 0*100% = 0% (black)
   * for --i: 1 (even), the lightness is 1*100% = 100% (white)* /
  color: hsl(0, 0%, calc(var(--i)*100%));

  &:nth-child(2n) { --i: 1 }
}

我们还希望奇数段落右对齐,同时保持偶数段落左对齐。为了实现这一点,我们引入了一个 --parity 变量,我们通常情况下不显式设置它——仅在偶数项中设置。我们在通常情况下设置的是我们之前的变量 --i。我们将其设置为 --parity 的值,并使用 0 作为回退值。

p {
  --i: var(--parity, 0);
  color: hsl(0, 0%, calc(var(--i)*100%));

  &:nth-child(2n) { --parity: 1 }
}

到目前为止,这与我们代码的先前版本完全相同。但是,如果我们利用以下事实:我们可以为相同变量在不同位置使用不同的回退值,那么我们也可以将 text-align 设置为 --parity 的值,并使用… right 作为回退值!

text-align: var(--parity, right)

在通常情况下,我们没有显式设置 --paritytext-align 使用回退值 right,这是一个有效值,因此我们有右对齐。但是,对于偶数项,我们显式将 --parity 设置为 1,这对于 text-align 不是有效值。这意味着 text-align 会恢复到其 initial 值,即 left

查看 thebabydino 在 CodePen 上创建的 Pen@thebabydino)。

现在,我们对奇数项实现了右对齐,对偶数项实现了左对齐,同时仍然只使用了一个 CSS 变量!

剖析更复杂的示例

假设我们希望获得以下结果。

Screenshot. Shows a bunch of numbered cards. Odd ones have the numbering on the left, while even ones have it on the right. Odd ones are right-aligned, while even ones are left-aligned. Odd ones are shifted a bit to the right and have a bit of a clockwise rotation, while even ones are shifted and rotated by the same amounts, but in the opposite directions. All have a grey to orange gradient background, but for the odd ones, this gradient goes from left to right, while for the even ones it goes from right to left.
编号卡片,其中偶数卡片的样式相对于奇数卡片是对称的。

我们使用段落元素 <p> 创建这些卡片,每个卡片一个。我们将其 box-sizing 切换为 border-box,然后为其设置 widthmax-widthpaddingmargin。我们还更改了默认的 font

查看 thebabydino 在 CodePen 上创建的 Pen@thebabydino)。

我们还添加了一个虚拟轮廓,以便查看这些元素的边界。

接下来,让我们使用 CSS 计数器:before 伪元素添加编号。

p {
  /* same code as before */
  counter-increment: c;
  
  &:before { content: counter(c, decimal-leading-zero) }
}

查看 thebabydino 在 CodePen 上创建的 Pen@thebabydino)。

现在,我们将为段落提供 flex 布局并增加编号的大小。

p {
  /* same code as before */
  display: flex;
  align-items: center;
  
  &:before {
    font-size: 2em;
    content: counter(c, decimal-leading-zero);
  }
}

查看 thebabydino 在 CodePen 上创建的 Pen@thebabydino)。

现在到了有趣的部分!

我们设置了一个切换 --i,它会根据奇偶性更改值——对于奇数项为 0,对于偶数项为 1

p {
  /* same code as before */
  --i: 0;
  
  &:nth-child(2n) { --i: 1 }
}

接下来,我们希望编号对于奇数项位于左侧,对于偶数项位于右侧。我们可以通过 order 属性来实现。此属性的初始值为 0,对于 :before 伪元素和段落文本内容都是如此。如果我们将偶数元素的编号(:before 伪元素)的 order 属性设置为 1,则此操作会将编号移动到内容之后。

p {
  /* same code as before */
  --i: 0;
  
  &:before {
    /* same code as before */
    /* we don't really need to set order explicitly as 0 is the initial value */
    order: 0;
  }
  
  &:nth-child(2n) {
    --i: 1;
    
    &:before { order: 1 }
  }
}

您可能会注意到,在这种情况下,order 值与切换 --i 值相同,因此为了简化操作,我们将 order 设置为切换值。

p {
  /* same code as before */
  --i: 0;
  
  &:before {
    /* same code as before */
    order: var(--i)
  }
  
  &:nth-child(2n) { --i: 1 }
}

查看 thebabydino 在 CodePen 上创建的 Pen@thebabydino)。

现在,我们希望在编号和段落文本之间留出一些间距(假设为 $gap)。这可以通过 :before 上的横向 margin 来实现。

对于奇数项,项目编号位于左侧,因此我们需要一个非零 margin-right。对于偶数项,项目编号位于右侧,因此我们需要一个非零 margin-left

当奇数项的奇偶性切换值为 0 时,左外边距为 0 = 0*$gap,而右外边距为 $gap = 1*$gap = (1 - 0)*$gap

类似地,对于偶数项,当奇偶性切换值为 1 时,左外边距为 $gap = 1*$gap,而右外边距为 0 = 0*$gap = (1 - 1)*$gap

两种情况下,结果都是 margin-left 等于奇偶切换值乘以边距值 ($gap),而 margin-right 等于 1 减去奇偶切换值,所有结果都乘以边距值。

$gap: .75em;

p {
  /* same code as before */
  --i: 0;
  
  &:before {
    /* same code as before */
    margin: 
      0                            /* top */
      calc((1 - var(--i))*#{$gap}) /* right */
      0                            /* bottom */
      calc(var(--i)*#{$gap})       /* left */;
  }
  
  &:nth-child(2n) { --i: 1 }
}

如果我们在多个地方使用互补值 (1 - var(--i)),那么最好将其设置为另一个 CSS 变量 --j

$gap: .75em;

p {
  /* same code as before */
  --i: 0;
  --j: calc(1 - var(--i));
  
  &:before {
    /* same code as before */
    margin: 
      0                      /* top */
      calc(var(--j)*#{$gap}) /* right */
      0                      /* bottom */
      calc(var(--i)*#{$gap}) /* left */;
  }
  
  &:nth-child(2n) { --i: 1 }
}

查看 thebabydino 在 CodePen 上创建的 Pen (@thebabydino)。

接下来,我们想为这些项目提供一个合适的 background。这是一个灰色到橙色的渐变,对于奇数项(奇偶切换 --i: 0),从左到右(或沿着 90deg 角)渐变,对于偶数项(奇偶切换 --i: 1),从右到左(沿着 -90deg 角)渐变。

这意味着渐变角度的绝对值相同 (90deg),只有符号不同——对于奇数项 (--i: 0) 为 +1,对于偶数项 (--i: 1) 为 -1

为了切换符号,我们使用在第一篇文章中介绍的方法。

/*
 * for --i: 0, we have 1 - 2*0 = 1 - 0 = +1
 * for --i: 1, we have 1 - 2*1 = 1 - 2 = -1
 */
--s: calc(1 - 2*var(--i))

这样,我们的代码就变成了

p {
  /* same code as before */
  --i: 0;
  --s: calc(1 - 2*var(--i));
  background: linear-gradient(calc(var(--s)*90deg), #ccc, #f90);
  
  &:nth-child(2n) { --i: 1 }
}

我们还可以移除虚拟轮廓,因为我们现在不需要它了。

查看 thebabydino 在 CodePen 上创建的 Pen (@thebabydino)。

接下来,我们对 transform 属性做类似的操作。

奇数项向右(x 轴正方向)稍微平移,并顺时针(正方向)稍微旋转,而偶数项向左(x 轴负方向)稍微平移,并逆时针(负方向)稍微旋转。

平移和旋转量相同,只有符号不同。

对于奇数项,transform 链为

translate(10%) rotate(5deg)

而对于偶数项,我们有

translate(-10%) rotate(-5deg)

使用我们的符号 --s 变量,统一的代码为

p {
  /* same code as before */
  --i: 0;
  --s: calc(1 - 2*var(--i));
  transform: translate(calc(var(--s)*10%)) 
             rotate(calc(var(--s)*5deg));
  
  &:nth-child(2n) { --i: 1 }
}

现在开始有点样子了!

查看 thebabydino 在 CodePen 上创建的 Pen (@thebabydino)。

下一步是圆角卡片。对于奇数卡片,我们希望左侧的角圆角半径为高度的一半。对于偶数项,我们希望右侧的角圆角半径相同。

鉴于我们不知道卡片的高度,我们只使用一个非常大的值,比如 50vh,由于 border-radius 的工作方式,它会被缩放到适合的大小。在我们的例子中,这意味着缩放到项目 height 的一半(因为垂直方向在同一侧都有顶部和底部圆角)和完整项目 width(因为水平方向只有一个圆角;要么在左侧要么在右侧,但不在左右两侧)中较小的那个。

这意味着我们希望左侧的角具有此半径 ($r: 50vh) 用于奇数项 (--i: 0),而右侧的角具有相同半径用于偶数项 (--i: 1)。因此,我们做了一些与编号 margin 案例非常相似的事情。

$r: 50vh;

p {
  /* same code as before */
  --i: 0;
  --j: calc(1 - var(--i));
  --r0: calc(var(--j)*#{$r});
  --r1: calc(var(--i)*#{$r});
  /* clockwise from the top left */
  border-radius: var(--r0) /* top left */
                 var(--r1) /* top right */
                 var(--r1) /* bottom right */
                 var(--r0) /* bottom left */;
  
  &:nth-child(2n) { --i: 1 }
}

查看 thebabydino 在 CodePen 上创建的 Pen (@thebabydino)。

现在到了真正有趣的部分——文本对齐!我们希望奇数项中的文本右对齐,而偶数项中的文本左对齐。唯一的问题是 text-align 不接受数字值,因此我们无法使用加法或乘法技巧来帮助我们。

可以帮助我们的是结合使用 CSS 变量的回退值和无效值。为此,我们引入另一个奇偶变量 --p,并且实际上我们将此变量设置为偶数项的 1。与之前的 --i 不同,我们永远不会为一般情况显式设置 --p,因为我们希望此变量的不同回退值用于不同的属性

至于 --i,我们将其设置为 --p,并在一般情况下回退值为 0。这个 0 的回退值实际上是在一般情况下使用的值,因为我们从未在那里显式设置 --p。对于我们显式将 --p 设置为 1 的偶数情况,--i 也变为 1

同时,我们将 text-align 属性设置为 --p,并在一般情况下回退值为 right。在偶数情况下,我们显式地将 --p 设置为 1text-align 值变为无效(因为我们将 text-align 设置为 --p 的值,而 --p 现在是 1,这对于 text-align 不是有效值),因此文本恢复为左对齐。

p {
  /* same code as before */
  --i: var(--p, 0);
  text-align: var(--p, right);
  
  &:nth-child(2n) { --p: 1 }
}

这给了我们我们一直在追求的结果。

查看 thebabydino 在 CodePen 上创建的 Pen (@thebabydino)。

处理响应式

虽然我们的卡片示例在较宽的屏幕上看起来很棒,但在缩小屏幕时却并非如此。

Screenshot collage. Since the width of the cards depends on the viewport width, the viewport may get too narrow to allow for displaying the numbering and the paragraph text side by side and the right one of the two overflows in this case.
宽屏结果(左)与窄屏结果(右)

为了解决这个问题,我们引入了另外两个自定义属性 --wide--k 来在宽屏和窄屏案例之间切换。在一般情况下,我们将 --k 设置为 --wide,回退值为 0,然后如果视口宽度为 340px 及以上,则将 --wide 设置为 1

p {
  /* same code as before */
  --k: var(--wide, 0);
  
  @media (min-width: 340px) { --wide: 1 }
}

由于我们只希望在宽屏情况下对项目进行转换并圆角,因此我们将平移、旋转和半径值乘以 --k(除非视口很宽,否则为 0,这会将其值切换为 1)。

p {
  /* same code as before */
  --k: var(--wide, 0);
  --r0: calc(var(--k)*var(--j)*#{$r});
  --r1: calc(var(--k)*var(--i)*#{$r});
  border-radius: var(--r0) /* top left */
                 var(--r1) /* top right */
                 var(--r1) /* bottom right */
                 var(--r0) /* bottom left */;
  transform: translate(calc(var(--k)*var(--s)*10%)) 
             rotate(calc(var(--k)*var(--s)*5deg));

  @media (min-width: 340px) { --wide: 1 }
}

稍微好一点,但在窄视口中我们的内容仍然溢出。我们可以通过仅在宽屏情况下将编号(:before 伪元素)放置在左侧或右侧来解决此问题,然后在窄屏情况下将其移动到卡片上方。

为此,我们将它的 order 和它的横向 margin 值都乘以 --k(在宽屏情况下为 1,否则为 0)。

我们还将 flex-direction 设置为 --wide,回退值为 column

这意味着 flex-direction 值在一般情况下为 column(因为我们没有在其他地方显式设置 --wide)。但是,如果视口很宽 (min-width: 340px),则我们的 --wide 变量将设置为 1。但 1 对于 flex-direction 是一个无效值,因此此属性恢复到其初始值 row

p {
  /* same code as before */
  --k: var(--wide, 0);
  flex-direction: var(--wide, column);
  
  &:before {
    /* same code as before */
    order: calc(var(--k)*var(--i));
    margin: 
      0                               /* top */
      calc(var(--k)*var(--j)*#{$gap}) /* right */
      0                               /* bottom */
      calc(var(--k)*var(--i)*#{$gap}) /* left */;
  }
  
  @media (min-width: 340px) { --wide: 1 }
}

结合在 body 上设置 min-width160px,我们现在消除了溢出问题。

响应式卡片,无溢出 (实时演示)。

我们还可以做的一件事是调整 font-size,使其也依赖于 --k

p {
  /* same code as before */
  --k: var(--wide, 0);
  font: 900 calc(var(--k)*.5em + .75em) cursive;

  @media (min-width: 340px) { --wide: 1 }
}

就是这样,我们的演示现在具有良好的响应能力!

响应式卡片,窄屏字体更小,无溢出 (实时演示)。

更多快速示例!

让我们再看一些使用相同技术的演示,但快速地不做从头开始构建。我们只会快速浏览它们背后的基本思想。

圆盘切片

圆盘切片 (实时演示)。

就像我们一起完成的卡片示例一样,我们可以使用 :before 伪元素进行编号,并在段落上使用 flex 布局。 圆盘切片效果 是使用 clip-path 实现的。

段落元素本身——水平偏移量、创建阴影效果的 radial-gradient() 的位置和强度、linear-gradient() 的方向及其停止点的饱和度、color 和文本对齐——都取决于 --parity 变量。

p {
  /* other styles not relevant here */
  --p: var(--parity, 1);
  --q: calc(1 - var(--p));
  --s: calc(1 - 2*var(--p)); /* sign depending on parity */
  transform: translate((calc(var(--i)*var(--s)*#{-$x})));
  background: 
    radial-gradient(at calc(var(--q)*100%) 0, 
      rgba(0, 0, 0, calc(.5 + var(--p)*.5)), transparent 63%) 
      calc(var(--q)*100%) 0/ 65% 65% no-repeat, 
    linear-gradient(calc(var(--s)*-90deg), 
      hsl(23, calc(var(--q)*98%), calc(27% + var(--q)*20%)), 
      hsl(44, calc(var(--q)*92%), 52%));
  color: HSL(0, 0%, calc(var(--p)*100%));
  text-align: var(--parity, right);
	
  &:nth-child(odd) { --parity: 0 }
}

对于编号(段落的 :before 伪元素),我们有 marginorder 都以与卡片示例完全相同的方式依赖于 --parity

如果视口 width 小于圆盘直径 $d 加上水平切片偏移量的绝对值的两倍 $x,那么我们就不再处于 --wide 案例中了。这会影响我们段落的 widthpaddingmargin,以及它们的水平偏移量和形状(因为我们不会剪裁它们以在此时获得圆盘切片效果)。

body {
  /* other styles not relevant here */
  --i: var(--wide, 1);
  --j: calc(1 - var(--i));
	
  @media (max-width: $d + 2*$x) { --wide: 0 }
}

p {
  /* other styles not relevant here */
  margin: calc(var(--j)*.25em) 0;
  padding: 
    calc(var(--i)*#{.5*$r}/var(--n) + var(--j)*5vw) /* vertical */
    calc(var(--i)*#{.5*$r} + var(--j)*2vw) /* horizontal */;
  width: calc(var(--i)*#{$d} /* wide */ + 
              var(--j)*100% /* not wide */);
  transform: translate((calc(var(--i)*var(--s)*#{-$x})));
  clip-path: 
    var(--wide, 
        
      /* fallback, used in the wide case only */
      circle($r at 50% calc((.5*var(--n) - var(--idx))*#{$d}/var(--n))));
}

我们在 270px 以下处于窄屏情况,并且在段落上具有 flex-directioncolumn。我们还将编号的横向边距和 order 都清零。

body {
  /* other styles not relevant here */
  --k: calc(1 - var(--narr, 1));
	
  @media (min-width: 270px) { --narr: 0 }
}

p {
  /* other styles not relevant here */
  flex-direction: var(--narr, column);

  &:before {
    /* other styles not relevant here */
    margin: 
      0                             /* top */
      calc(var(--k)*var(--q)*.25em) /* right */
      0                             /* bottom */
      calc(var(--k)*var(--p)*.25em) /* left */;
    order: calc(var(--k)*var(--p));
  }
}

四步信息图

Screenshot collage. On the left, there's the wide screen scenario. In the middle, there's the normal screen scenario. On the right, there's the narrow screen scenario.
一个四步信息图 (实时演示)。

这与前两个示例的工作原理基本相同。我们在段落上使用 flex 布局,在窄屏情况下使用 column 方向。在同一情况下,我们还有更小的 font-size

body {
  /* other styles not relevant here */
  --k: var(--narr, 1);
  
  @media (min-width: 400px) { --narr: 0 }
}

p {
  /* other styles not relevant here */
  flex-direction: var(--narr, column);
  font-size: calc((1.25 - .375*var(--k))*1em);
}

奇偶性决定每个段落的文本对齐方式,哪个横向 border 获取非零值,以及 border 渐变的位置和方向。奇偶性和我们是否处于宽屏情况都会决定横向边距和填充。

body {
  /* other styles not relevant here */
  --i: var(--wide, 1);
  --j: calc(1 - var(--i));
  
  @media (max-width: $bar-w + .5*$bar-h) { --wide: 0 }
}

p {
  /* other styles not relevant here */
  margin: 
    .5em                                 /* top */
    calc(var(--i)*var(--p)*#{.5*$bar-h}) /* right */
    0                                    /* bottom */
    calc(var(--i)*var(--q)*#{.5*$bar-h}) /* left */;
  border-width: 
    0                        /* top */
    calc(var(--q)*#{$bar-b}) /* right */
    0                        /* bottom */
    calc(var(--p)*#{$bar-b}) /* left */;
  padding: 
    $bar-p                                         /* top */
    calc((var(--j) + var(--i)*var(--q))*#{$bar-p}) /* right */
    $bar-p                                         /* bottom */
    calc((var(--j) + var(--i)*var(--p))*#{$bar-p}) /* left */;
  background: 
    linear-gradient(#fcfcfc, gainsboro) padding-box, 
    linear-gradient(calc(var(--s)*90deg), var(--c0), var(--c1)) 
      calc(var(--q)*100%) /* background-position */ / 
      #{$bar-b} 100% /* background-size */;
  text-align: var(--parity, right);
}

图标使用:before伪元素创建,其order取决于奇偶性,但仅当我们不在窄屏场景下时——在这种情况下,它始终位于段落实际文本内容之前。其横向margin取决于奇偶性和我们是否处于宽屏情况下。将其定位在其父段落一半之外的大值组件仅在宽屏情况下存在。font-size也取决于我们是否处于窄屏情况下(这会影响其em尺寸和填充)。

order: calc((1 - var(--k))*var(--p));
margin: 
  0                                                          /* top */
  calc(var(--i)*var(--p)*#{-.5*$ico-d} + var(--q)*#{$bar-p}) /* right */
  0                                                          /* bottom */
  calc(var(--i)*var(--q)*#{-.5*$ico-d} + var(--p)*#{$bar-p}) /* left */;
font-size: calc(#{$ico-s}/(1 + var(--k)));

环使用绝对定位的:after伪元素创建(其位置取决于奇偶性),但仅限于宽屏情况。

content: var(--wide, '');

二维情况

Screenshot collage. On the left, we have the wide screen scenario. Each article is laid out as a 2x2 grid, with the numbering occupying an entire column, either on the right for odd items or on the left for even items. The heading and the actual text occupy the other column. In the middle, we have the normal screen case. Here, we also have a 2x2 grid, but the numbering occupies only the top row on the same column as before, while the actual text content now spans both columns on the second row. On the right, we have the narrow screen case. In this case, we don't have a grid anymore, the numbering, the heading and the actual text are one under the other for each article.
屏幕截图拼贴(在线演示,由于CSS变量和calc()错误,不支持Edge浏览器)。

这里我们有一堆article元素,每个元素都包含一个标题。让我们看看这个响应式布局是如何工作的最有趣方面!

在每个文章上,我们都有一个二维布局(grid)——但仅当我们不在窄屏场景下(--narr: 1)时,在这种情况下,我们会回退到正常的文档流,使用:before伪元素创建编号,然后是标题,然后是实际文本。在这种情况下,我们还会在标题上添加垂直填充,因为我们不再有网格间隙,我们不希望内容过于拥挤。

html {
  --k: var(--narr, 0);
	
  @media (max-width: 250px) { --narr: 1 }
}

article {
  /* other styles irrelevant here */
  display: var(--narr, grid);
}

h3 {
  /* other styles irrelevant here */
  padding: calc(var(--k)*#{$hd3-p-narr}) 0;
}

对于grid,我们创建了两列,其宽度取决于奇偶性和我们是否处于宽屏情况下。在宽屏情况下,我们将编号(:before伪元素)跨两行,根据奇偶性分别位于第二列或第一列。如果我们不在宽屏情况下,则段落跨第二行的两列。

在宽屏场景下,我们将grid-auto-flow设置为column dense,否则将其恢复为initialrow。由于我们的article元素比列的组合宽度和它们之间的列间隙更宽,因此我们使用place-content将实际的网格列定位在右侧或左侧,具体取决于奇偶性。

最后,我们将标题放置在列的末尾或开头,具体取决于奇偶性,以及如果我们在宽屏场景下,段落文本的对齐方式。

$col-1-wide: calc(var(--q)*#{$col-a-wide} + var(--p)*#{$col-b-wide});
$col-2-wide: calc(var(--p)*#{$col-a-wide} + var(--q)*#{$col-b-wide});

$col-1-norm: calc(var(--q)*#{$col-a-norm} + var(--p)*#{$col-b-norm});
$col-2-norm: calc(var(--p)*#{$col-a-norm} + var(--q)*#{$col-b-norm});

$col-1: calc(var(--i)*#{$col-1-wide} + var(--j)*#{$col-1-norm});
$col-2: calc(var(--i)*#{$col-2-wide} + var(--j)*#{$col-2-norm});

html {
  --i: var(--wide, 1);
  --j: calc(1 - var(--i));
	
  @media (max-width: $art-w-wide) { --wide: 0 }
}

article {
  /* other styles irrelevant here */
  --p: var(--parity, 1);
  --q: calc(1 - var(--p));
  grid-template-columns: #{$col-1} #{$col-2};
  grid-auto-flow: var(--wide, dense column);
  place-content: var(--parity, center end);
  
  &:before {
    /* other styles irrelevant here */
    grid-row: 1/ span calc(1 + var(--i));
    grid-column: calc(1 + var(--p))/ span 1;
  }
  
  &:nth-child(odd) { --parity: 0 }
}

h3 {
  /* other styles irrelevant here */
  justify-self: var(--parity, self-end);
}

p {
  grid-column-end: span calc(1 + var(--j));
  text-align: var(--wide, var(--parity, right));
}

我们还有数值,例如网格间隙、边框半径、填充、字体大小、渐变方向、旋转和平移方向,具体取决于奇偶性和/或我们是否处于宽屏情况下。

更多示例!

如果您想了解更多内容,我已经创建了一个完整的响应式演示集供您欣赏!

Screenshot of collection page on CodePen, showing the six most recent demos added.
响应式演示集。