使用 CSS 自定义属性构建超级强大的网格组件

Avatar of Michelle Barker
Michelle Barker

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

不久前,我写了一篇广受好评的文章,介绍了 将 CSS 变量与 CSS 网格结合使用 以帮助构建更易于维护的布局。但 CSS 网格不仅仅用于页面!这是一个常见的误解。虽然它确实非常适合页面布局,但我发现自己在构建组件时也同样频繁地使用网格。在这篇文章中,我将讨论在组件级别使用 CSS 网格。

网格 既不是 Flexbox 的替代品,反之亦然。事实上,将两者结合使用,在构建组件时能赋予我们更强大的功能。

构建简单的组件

在本演示中,我将逐步介绍如何构建一个文本和图像组件,这是您在典型网站上可能会经常遇到的组件,而且我自己也经常构建这种组件。

这是我们的第一个组件应该是什么样子

带有文本和图像的简单组件

让我们假设构成我们组件的元素需要与页面上的其他组件对齐到相同的网格。这是相同的组件,覆盖了网格列(忽略背景)

带有网格叠加的相同组件

您应该会看到一个包含 14 列的网格——12 列固定宽度的中心列和两列弹性列,分别位于两侧。(这些是为了说明目的,在浏览器中不可见。)在元素上使用 display: grid 允许我们将该元素的直接子元素放置在网格容器中。为了保持简单,我们的组件只有两个网格子元素——一个图像和一段文本。图像从第 2 行开始,在列轴上跨越 7 个轨道。文本块从第 9 行开始,跨越 5 列。

首先,也是非常重要的一点,我们需要从语义标记开始。尽管网格使我们能够将子元素放置在任何我们选择的布局中,但为了可访问性和不支持网格的浏览器,底层标记应遵循逻辑顺序。以下是我们将用于网格组件的标记

<article class="grid">
  <figure class="grid__media">
    <img src="" alt="" width="800px" height="600px" />
  </figure>
  <div class="grid__text">
    <h3>Heading</h3>
    <p>Lorem ipsum...</p>
  </div>
</article>

.grid__media.grid__text 类将用于定位子项。首先,我们需要定义网格容器。我们可以使用以下 CSS 来调整网格轨道的尺寸

.grid {
  display: grid;
  grid-template-columns: 
    minmax(20px, 1fr) repeat(12, minmax(0, 100px)) minmax(20px, 1fr);
  grid-column-gap: 20px;
}

这将提供我们之前演示的网格——12 列中心列,最大宽度为 100px,两侧各有一列弹性列,每列之间有 20px 的间隙。如果您对 minmax() 函数感到好奇,我写过一篇 关于它的文章,其中解释了我们网格列中正在发生的事情。

在本例中,由于我们的图像和文本块都将在同一网格行轨道上(并且我希望轨道大小只使用默认的 auto,这将根据内容的大小调整行),因此我不需要在这里声明我的网格行大小。但是,明确地将网格项放置在同一行将使处理不同的组件变体变得更容易,我们稍后会介绍这一点。

如果您是 Grid 的新手,并且正在寻找入门方法,我建议您访问 gridbyexample.com。现在不必太担心 grid-template-columns 声明——本文中的许多要点也可以应用于更简单的网格。

现在,我们需要使用以下 CSS 来放置网格项。网格为我们提供了各种不同的放置项的方法。在本例中,我使用的是 grid-column,它是 grid-column-start / grid-column-end 的简写

.grid__media {
  grid-column: 2 / 9; /* Start on Column 2 and end at the start of Column 9 */
}

.grid__text {
  grid-column: 10 / 14; /* Start on Column 10 and end at the start of Column 14 */
}

.grid__media,
.grid__text {
  grid-row: 1; /* We want to keep our items on the same row */
}

如果您在编码时发现很难想象网格线,在 CSS 注释中用 ASCII 绘制它 是一种方便的方法。

这是一个组件运行的演示——您可以切换背景的显示与隐藏来显示网格列

查看 CodePen 上 Michelle Barker (@michellebarker) 编写的笔 具有可切换背景的网格组件

组件变体

构建组件非常简单,但是如果我们有许多相同组件的不同变体呢?以下是一些我们可能想要使用的不同布局的示例

相同类型组件的四种不同布局变体

如您所见,文本块和图像在网格上的位置不同,并且图像块跨越的列数也不同。

就我个人而言,我之前处理过多达 9 个变体的组件!在这种情况下编码网格可能会变得难以管理,尤其是在考虑回退和响应性时,因为我们的网格声明可能会变得难以扫描和调试。让我们看看如何使它更容易管理和维护。

一致性

首先,让我们确定哪些属性值将在组件变体之间保持一致,以便我们尽可能少地编写代码。在本例中,我们的文本块应始终跨越 4 列,而我们的图像可以更大或更小,并对齐到任何列。

对于我们的文本块,我将使用 span 声明——这样,我可以确保此块的宽度保持一致。对于我们的图像,我将继续使用起始行和结束行,因为图像跨越的列数以及其位置在不同的组件变体之间有所不同。

命名网格线

网格允许您 命名您的线条 并通过名称和数字来引用它们,我发现这在放置项目时非常有用。您可能不想过分地命名线条,因为您的网格声明可能会变得难以阅读,但命名一些常用的线条会很有用。在本例中,我使用的是 wrapper-startwrapper-end 名称。现在,当将项目放置在任何这些线条上时,我知道它们将与中心包装器对齐。

.grid {
  display: grid;
  grid-template-columns: [start] minmax(20px, 1fr) [wrapper-start] repeat(12, minmax(0, 100px)) [wrapper-end] minmax(20px, 1fr) [end];
}

使用 *-start*-end 为网格线名称添加前缀具有额外的优势,它允许我们通过网格区域来放置项目。通过在网格项上设置 grid-column: wrapper,它将放置在我们的 wrapper-startwrapper-end 线之间。

使用 CSS 变量进行放置

CSS 变量(也称为自定义属性)使我们能够为组件编写更易于维护的代码。它们使我们能够在 CSS 中设置和重用值。如果您使用 Sass 或 Less 等预处理器,则可能熟悉使用变量。CSS 变量在几个重要方面不同于预处理器变量

  • 它们是动态的。换句话说,一旦设置,值仍然可以更改。您可以更新相同的变量以采用不同的值,例如在不同的组件中或在不同的断点处。
  • 它们不能用于选择器名称、属性名称或媒体查询声明中。它们只能用于属性值(因此它们被称为自定义属性)。

CSS 变量的设置和使用方式如下

.box {
  --color: orange; /* Defines the variable and default value */
  background-color: var(--color); /* Applies the variable as a property value */
}

我们可以利用 CSS 变量允许默认值这一事实——这样,如果找不到变量,就会使用回退值。

您可以像这样为变量设置默认值

background-color: var(--color, red);

因此,在本例中,我们将 background-color 属性设置为 --color 的值,但如果找不到该变量(例如,它尚未声明),则背景将为红色。

在放置网格项的 CSS 代码中,我们可以使用一个变量并声明一个默认值,如下所示

.grid__media {
  grid-column: var(--mediaStart, wrapper-start) / var(--mediaEnd, 9);
}

.grid__text {
  grid-column: var(--textStart, 10) / span 4;
}

我们不需要为文本 span 声明使用变量,因为这对于每个组件变体都是相同的。

现在,在处理组件变体时,我们只需要在项目位置不同于原始位置时定义变量

.grid--2 {
  --mediaEnd: end;
  --textStart: wrapper-start;
}

.grid--2,
.grid--4 {
  --mediaStart: 8;
}

.grid--3 {
  --mediaStart: start;
}

.grid--4 {
  --mediaEnd: wrapper-end;
  --textStart: 3;
}

以下是结果

查看 Michelle Barker 在 CodePen 上的笔:使用变量的 CSS Grid 组件@michellebarker)。

为了比较,如果不使用变量,以相同方式放置项目将如下所示

/* Grid 2 */
.grid--2 .grid__media {
  grid-column: 8 / end;
}

.grid--2 .grid__text {
  grid-column-start: wrapper-start;
}

/* Grid 3 */
.grid--3 .grid__media {
  grid-column-start: start;
}

/* Grid 4 */
.grid--4 .grid__media {
  grid-column: 8 / wrapper-end;
}

.grid--4 .grid__text {
  grid-column-start: 3;
}

区别很大,对吧?对我来说,使用变量的版本更易读,并且可以帮助我一目了然地了解每个组件变体的元素在哪里定位。

媒体查询中的变量

CSS 变量还允许我们通过在媒体查询中声明它们来轻松调整网格项目在不同断点处的定位。可以想象,我们可能希望为平板电脑尺寸的屏幕显示更简单的网格布局,并在屏幕尺寸达到桌面大小后切换到更复杂的布局。使用以下代码,我们可以为较小的屏幕显示简单的交替布局,然后将更复杂布局的变量包装在媒体查询中。

.grid__media {
  grid-column: var(--mediaStart, wrapper-start) / var(--mediaEnd, 8);
}

.grid__text {
  grid-column: var(--textStart, 8) / span var(--textSpan, 6);
}

.grid--2,
.grid--4 {
  --mediaStart: 8;
  --mediaEnd: 14;
  --textStart: wrapper-start;
}

/* Move to a more complex layout starting at 960px */
@media(min-width: 960px) {
  .grid {
    --mediaStart: wrapper-start;
    --mediaEnd: 9;
    --textStart: 10;
    --textSpan: 4;
  }
  
  .grid--2 {
    --mediaStart: 9;
    --mediaEnd: end;
    --textStart: wrapper-start;
  }
  
  .grid--2,
  .grid--4 {
    --mediaStart: 8;
  }

  .grid--3 {
    --mediaStart: start;
  }

  .grid--4 {
    --mediaEnd: wrapper-end;
    --textStart: 3;
  }
}

这是完整的演示效果

查看 Michelle Barker 在 CodePen 上的笔:使用变量和媒体查询的 CSS Grid 组件@michellebarker)。

性能

使用 CSS 变量时需要注意的一点是,更改元素上的变量会导致对该元素的所有子元素重新计算样式(这是一篇优秀的文章,详细介绍了变量的性能)。因此,如果您的组件有许多子元素,那么这可能需要考虑,尤其是在您想要多次更新变量的情况下。

结论

使用 CSS 网格处理响应式设计有很多不同的方法,这当然不是唯一的方法。我也不会建议在项目的每个部分都为所有网格代码使用变量——这可能会非常混乱,而且很可能没有必要!但是,在过去一年中大量使用 CSS 网格后,我可以证明这种方法在处理多个组件变体时非常有效地保持代码的可维护性。