仅用 20 行 CSS 代码创建响应式网格杂志布局

Avatar of Keir Watson
Keir Watson 发表

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

我最近正在做一个博客链接的现代版本。其想法是为读者提供这些博客最新文章的选择,以杂志风格的布局呈现,而不是仅仅在侧边栏中列出我们最喜欢的博客。

轻松的部分是从我们最喜欢的 RSS 提要中获取文章列表和摘录。为此,我们使用了 WordPress 插件 Feedzy lite,它可以将多个提要聚合到一个按时间排序的列表中——非常适合展示它们的最新内容。困难的部分是让它看起来很棒。

插件的默认列表 UI 非常单调,所以我想将其样式化,使其看起来像报纸或杂志网站,混合使用较小和较大尺寸的“特色内容”面板。

这似乎是 CSS Grid 的理想用例!为不同的布局创建网格布局,例如,一个五列布局和一个三列布局,然后使用媒体查询在不同的断点之间切换。对吧?但是,当我们可以使用 grid 的 auto-fit 选项自动为我们创建一个流畅的响应式网格时,我们真的需要这些媒体查询——以及识别断点的所有麻烦吗?

这种方法听起来很有诱惑力,但是当我开始引入跨列元素时,我遇到了网格在窄屏幕上溢出的问题。媒体查询似乎是唯一的解决方案。也就是说,直到我找到了一种解决方法!

在查看了几个关于 CSS Grid 的教程后,我发现它们主要分为两类

  1. 向您展示如何使用跨列元素创建有趣的布局,但适用于固定数量的列。
  2. 解释如何制作响应式网格,该网格可以自动调整大小,但所有网格项的宽度相同(即没有跨列)。

我想让网格同时做到这两点:创建一个完全响应的流畅布局,其中也包括响应式调整大小的多列元素。

美妙之处在于,一旦您了解了响应式网格的局限性,以及跨列为何以及何时会破坏网格响应性,就可以仅用 **十几行代码** 加上一个简单的媒体查询(或者如果您愿意限制跨列选项,甚至无需媒体查询)来定义响应式杂志/新闻风格的布局。

下面是一个可视化展示,展示了 RSS 插件开箱即用的样子,以及我们对其进行样式化后的样子。

(演示)

这种杂志风格的网格布局是完全响应式的,彩色特色面板会随着列数的变化而动态调整。页面显示大约 50 篇文章,但布局代码与显示的文章数量无关。将插件提升到显示 100 篇文章,布局在一直保持有趣。

所有这些都仅使用 CSS 实现,并且只有一个媒体查询来处理最窄屏幕(即小于 460px)上的单列显示。

令人难以置信的是,这个布局只用了 21 行 CSS 代码(不包括全局内容样式)。但是,为了在这么少的代码行中实现这种灵活性,我不得不深入研究 CSS Grid 中一些较为模糊的部分,并学习如何解决其固有的某些限制。

产生此布局的代码的基本元素非常短,证明了 CSS Grid 的强大功能。

.archive {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  grid-gap: 32px;
  grid-auto-flow: dense;
}

/* Extra-wide grid-posts */
.article:nth-child(31n + 1) {
  grid-column: 1 / -1;
}
.article:nth-child(16n + 2) {
  grid-column: -3 / -1;
}
.article:nth-child(16n + 10) {
  grid-column: 1 / -2;
}

/* Single column display for phones */
@media (max-width: 459px) {
  .archive {
    display: flex;
    flex-direction: column;
  }
}

本文中的技术同样可以用于设置任何动态生成的内容的样式,例如最新文章小部件、归档页面或搜索结果的输出。

创建响应式网格

我设置了 17 个项目,显示各种模拟内容——标题、图像和摘录——所有这些都包含在一个包装器中。

<div class="archive">
  <article class="article">
    <!-- content -->
  </article>
  
  <!-- 16 more articles -->
  
</div>

将这些项目转换为响应式网格的代码非常紧凑。

.archive {
  /* Define the element as a grid container */
  display: grid;
  /* Auto-fit as many items on a row as possible without going under 180px */
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  /* A little spacing between articles */
  grid-gap: 1em;
}

注意行高是如何自动调整以适应行中最高内容的。如果您更改 Pen 的宽度,您将看到项目流畅地增长和缩小,并且列数分别从 1 更改为 5。

这里使用的 CSS Grid 魔法是 auto-fit 关键字,它与应用于 grid-template-columnsminmax() 函数配合使用。

工作原理

我们可以仅使用以下方法实现五列布局

.archive {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
}

但是,这将创建五列,这些列会随着不同屏幕宽度的变化而增长和缩小,但始终保持五列,导致它们在小屏幕上变得非常窄。第一个想法可能是创建一堆媒体查询并使用不同数量的列重新定义网格。这可以正常工作,但是使用 auto-fit 关键字,所有这些都自动完成。

为了使 auto-fit 按我们想要的方式工作,我们需要使用 minmax() 函数。这告诉浏览器列可以压缩到多小,然后是它们可以扩展到的最大宽度。更小,它会自动减少列数。更大,列数增加。

.archive {
  grid-template-columns: repeat (auto-fit, minmax(180px, 1fr));
}

在此示例中,浏览器将尽可能多地放入 180px 宽的列。如果剩余空间,列将通过在它们之间共享剩余空间来平均增长——1fr 值表示:使列成为可用宽度的相等分数。

拖动窗口,随着可用空间的增加,所有列都会平均增长以利用任何额外的空间。列将继续增长,直到可用空间允许再增加一列 180px,此时将出现一整列新的列。减小屏幕宽度,该过程将反转,完美地调整网格,直至单列布局。太神奇了!

并且您只需一行代码即可获得所有这些响应能力。这有多酷?

使用“autoflow: dense”创建跨列

到目前为止,我们已经有了响应式网格,但所有项的宽度都相同。对于新闻或杂志布局,我们需要一些内容通过跨越两列或更多列甚至可能跨越所有列来进行特色展示。

要创建多列跨列,我们可以将跨列功能添加到我们想要占据更多空间的网格项中。例如,如果我们希望列表中的第三个项目宽两列,我们可以添加

.article:nth-child(3) {
  grid-column: span 2;
}

但是,一旦我们开始添加跨列,就会出现许多问题。首先,网格中可能会出现间隙,因为较宽的项目可能无法容纳在行中,因此网格 auto-fit 会将其推到下一行,留下它本应在的位置的间隙。

简单的解决方法是将 grid-auto-flow: dense 添加到网格元素中,这会告诉浏览器用其他项目填充任何间隙,有效地使较窄的内容像这样围绕较宽的项目流动。

请注意,项目现在是乱序的,第四个项目在宽度是两倍的第三个项目之前。据我所知,对此没有办法解决,这是您在使用 CSS Grid 时必须接受的限制之一。

查看 Geoff Graham 的 “网格的 Dense 关键字的自动流动能力”,以了解 grid-auto-flow: dense 的介绍以及它如何工作的示例。

指定跨列的方法

有多种方法可以指示项目应跨越多少列。最简单的方法是将 grid-columns: span [n] 应用于其中一项,其中 n 是元素将跨越的列数。我们布局中的第三个项目具有 grid-column: span 2,这解释了为什么它的宽度是仅跨越一列的其他项目的两倍。

其他方法需要您 明确定义网格线。网格线的编号系统如下所示

网格线可以使用正值(例如 1、2、3)从左到右指定,或使用负值(例如 -1、-2、-3)从右到左指定。这些值可用于使用 grid-column 属性将项目放置在网格上,如下所示

.grid-item {
  grid-column: (start track) / (end track);
}

因此,这为我们提供了其他方法来指定跨列的项目。这尤其灵活,因为起始值或结束值都可以替换为 span 关键字。例如,上面示例中的三列蓝色框可以通过向第八个网格项添加以下任何内容来创建

  • grid-column: 3 / 6
  • grid-column: -4 / -1
  • grid-column: 3 / span 3
  • grid-column: -4 / span 3
  • grid-column: span 3 / -1
  • 等等。

在非响应式(即固定列数)网格中,这些都产生相同的效果(如上图中的蓝色框),但是,如果网格是响应式的并且列数发生变化,它们的差异就会开始显现。某些列跨度会破坏自动流式网格的布局,使得这两种技术看起来不兼容。幸运的是,有一些解决方案可以让我们成功地将两者结合起来。

然而,首先我们需要理解问题所在。

溢出侧滚动问题

以下是一些使用上述表示法创建的特色区域

(演示)

在全宽(五列)时,它看起来都很好,但是当调整大小到应该是两列时,布局会像这样中断

如您所见,我们的网格失去了响应能力,尽管容器已经缩小,但网格试图保持所有五列。为此,它放弃了尝试保持等宽列,并且网格从其容器的右侧溢出,导致水平滚动。

为什么会这样?问题在于浏览器试图遵循我们命名的显式网格线。在这个宽度下,auto-fit 网格应该隐式地显示两列,但是我们的网格线编号系统与之矛盾,因为它显式地引用了第五条网格线。这种矛盾导致了混乱。为了正确显示我们隐式的两列网格,唯一允许的线号是 1、2 和 3 以及 -3、-2、-1,如下所示

但是,如果我们的任何网格项包含位于此范围之外的grid-column引用,例如网格线编号 4、5 或 6(或 -4、-5 或 -6),浏览器就会收到混合信息。一方面,我们要求它自动创建灵活的列(这应该隐式地为我们提供在这个屏幕宽度下的两列),但我们也显式地引用了在两列网格中没有出现的网格线。当隐式(自动)列与显式列数之间存在冲突时,**网格始终优先考虑显式网格**;因此出现了不需要的列和水平溢出(这也恰当地被称为CSS 数据丢失)。就像使用网格线编号一样,跨度也可以创建显式列。因此,grid-column: span 3(演示中的第八个网格项)强制网格显式地采用至少三列,而我们希望它隐式地显示两列。

在这一点上,似乎唯一前进的道路是使用媒体查询来更改布局中断时的 grid-column 值——但不要太快!这是我最初的假设。但在更多地思考并尝试各种选项后,我发现有一组有限的解决方法可以一直使用到两列,只需一个媒体查询来覆盖最窄屏幕的单列布局。

解决方案

我意识到,诀窍是仅使用出现在您打算显示的最窄网格中的网格线来指定跨度。在这种情况下,是一个两列网格。(我们将使用媒体查询来覆盖非常窄屏幕的单列场景。)这意味着我们可以安全地使用网格线 1、2 和 3(或 -3、-2 和 -1)而不会破坏网格。

我最初认为这意味着将自己限制在最多两列的跨度,使用以下组合

  • grid column: span 2
  • grid-column: 1 /3
  • grid-column: -3 / -1

这保持了完全的响应性,一直到两列

尽管这有效,但从设计的角度来看,它相当有限,而且不是特别令人兴奋。我希望能够创建在大型屏幕上宽度为三列、四列甚至五列的跨度。但是怎样才能做到呢?我的第一个想法是我必须求助于媒体查询(天哪,旧习惯真难改!),但我试图摆脱这种方法,并以不同的方式思考响应式设计。

再次查看我们仅使用 1 到 3 和 -3 到 -1 可以做的事情,我逐渐意识到我可以混合正负线号作为网格列的起始和结束值,例如 1/-32/-2。乍一看,这似乎不是很有趣。当你意识到当你在调整网格大小时这些线的位置时,情况就发生了变化:这些跨度元素会随着屏幕尺寸的变化而改变宽度。这为响应式列跨度开辟了一系列新的可能性:随着屏幕变宽,跨越不同列数的项目,而无需使用媒体查询。

我发现的第一个例子是 grid-column: 1/-1。这使得项目像一个全宽横幅一样,跨越所有列数中的第一列到最后一列。它甚至适用于一列宽!

通过使用 grid-column: 1/-2,可以创建一个左对齐的几乎全宽跨度,它始终在其右侧留出一个一列的项目。当缩小到两列时,它会响应地缩小到一列。令人惊讶的是,它在缩小到单列布局时也能正常工作。(原因似乎是网格不会将项目折叠到零宽度,因此它保持一列宽,就像 grid-column: 1/1 一样。)我假设 grid-column: 2/-1 会以类似的方式工作,但与右侧边缘对齐,并且在大多数情况下确实如此,除了在一列显示时会导致溢出。

接下来我尝试了 1/-3,它在较宽的屏幕上效果很好,显示至少三列,在较小的屏幕上显示一列。我认为它在两列网格上会做一些奇怪的事情,因为第一条网格线与 -3 的网格线相同。令我惊讶的是,它仍然作为单列项目正常显示。

经过大量的尝试,我想出了 11 个可能的网格列值,使用来自两列网格的网格线编号。令人惊讶的是,其中三个可以一直使用到单列布局。另外七个可以一直使用到两列,并且只需要一个媒体查询来处理单列显示。

以下是完整列表

响应式网格列值,显示它们在 auto-fit 网格中不同屏幕尺寸下的显示方式。(演示)

如您所见,尽管这是所有可能的响应式跨度的一个有限子集,但实际上有很多可能性。

  • 2/-2 很有趣,因为它创建了一个居中的跨度,可以一直使用到一列!
  • 3/-1 最不实用,因为它即使在两列时也会导致溢出。
  • 3/-3 令人意外。

通过使用此列表中的各种 grid-column 值,可以创建有趣且完全响应式的布局。使用一个用于最窄单列显示的媒体查询,我们可以使用十种不同的 grid-column 跨度模式。

单列媒体查询通常也很简单。此最终演示中的一个在较小的屏幕上恢复使用 flexbox

@media (max-width: 680px) {
  .archive {
    display: flex;
    flex-direction: column;
  }

  .article {
    margin-bottom: 2em;
  }
}

这是最终网格,如您所见,它从一列到五列都完全响应

(演示)

使用 :nth-child() 重复可变长度显示

我用来将代码减少到几十行的最后一个技巧是 :nth-child(n) 选择器,我用它来设置网格中多个项目的样式。我希望我的跨度样式应用于我的 Feed 中的多个项目,以便特色帖子框定期出现在页面中。首先,我使用了逗号分隔的选择器列表,如下所示

.article:nth-child(2),
.article:nth-child(18),
.article:nth-child(34),
.article:nth-child(50)  {
  background-color: rgba(128,0,64,0.8);
  grid-column: -3 / -1;
}

但很快我发现这很麻烦,尤其是我必须为每个想要在每篇文章中设置样式的子元素重复此列表——例如标题、链接等等。在原型设计期间,如果我想更改跨度元素的位置,我必须手动更改这些列表中的每个数字,这既乏味又容易出错。

那时我意识到我可以使用功能强大的 :nth-child 伪选择器,而不是像上面列表中使用的那样简单的整数。:nth-child(n) 也可以接受一个等式,例如 :nth-child(2n+ 2),它将定位每个第二个子元素。

以下是如何使用 :nth-child([formula]) 在我的网格中创建蓝色全宽面板,这些面板出现在页面顶端,并在页面下方重复出现。

.article:nth-child(31n + 1) {
  grid-column: 1 / -1;
  background: rgba(11, 111, 222, 0.5);
}

括号中的部分 (31n + 1) 确保选择第 1 个、第 32 个、第 63 个等子元素。浏览器从 n=0 开始循环(在这种情况下 31 * 0 + 1 = 1),然后 n=1 (31 * 1 + 1 = 32),然后 n=2 (31 * 2 + 1 = 63)。在最后一种情况下,浏览器意识到没有第 63 个子项,因此它忽略该项,停止循环,并将 CSS 应用于第 1 个和第 32 个子元素。

对于从右到左交替出现在页面上的紫色框,我做了类似的事情。

.article:nth-child(16n + 2) {
  grid-column: -3 / -1;
  background: rgba(128, 0, 64, 0.8);
}

.article:nth-child(16n + 10) {
  grid-column: 1 / -2;
  background: rgba(128, 0, 64, 0.8);
}

第一个选择器用于右侧的紫色框。16n + 2 确保样式应用于每第 16 个网格项,从第二个项目开始。

第二个选择器定位右侧的框。它使用相同的间距 (16n),但偏移量不同 (10)。因此,这些框会定期出现在右侧,用于网格项 10、26、42 等。

在这些网格项及其内容的视觉样式方面,我使用了另一个技巧来减少重复。对于两个框共享的样式(例如 background-color),可以使用单个选择器来定位两者

.article:nth-child(8n + 2) {
  background: rgba(128, 0, 64, 0.8);
  /* Other shared syling */
}

这将定位项目 2、10、18、26、34、42、50 等。换句话说,它选择了左右两个特色框。

它之所以有效,是因为 8n 正好是 16n 的一半,并且因为两个单独选择器中使用的偏移量差 8(即 +10 和 +2 之间的差为 8)

最终想法

目前,CSS Grid 可用于创建灵活的响应式网格,代码量最少,但这确实对元素定位施加了一些重大限制,除非使用媒体查询进行倒退操作。

能够指定不会在较小屏幕上强制溢出的跨度将非常棒。目前,我们实际上是在告诉浏览器,“请创建一个响应式网格”,它确实做得非常漂亮。但是,当我们继续说,“哦,并且让这个网格项目跨越四列”时,它在窄屏幕上就会出现问题,优先考虑四列跨度请求而不是响应式网格。能够告诉网格优先考虑响应能力而不是我们的跨度请求将非常棒。类似这样

.article {
  grid-column: span 3, autofit;
}

响应式网格的另一个问题是最后一行。随着屏幕宽度的变化,最后一行通常不会填满。我花了很长时间寻找一种方法来让最后一个网格项目跨越(并因此填满)剩余的列,但似乎您现在无法在 Grid 中做到这一点。如果我们可以使用像 auto 这样的关键字来指定项目的起始位置,这意味着“请将左边缘放在它落下的任何位置”,那就太好了。像这样

.article {
  grid-column: auto, -1;
}

……这将使左边缘跨越到行的末尾。