为你的博客打造酷炫的 CSS Grid 小技巧

Avatar of Ana Tudor
Ana Tudor

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

大约十年前,我尝试修改自己创建的博客的外观时,发现了 CSS。很快,我就能用更数学化的、因此更容易理解的功能(如转换)来编写 酷炫的东西。但是,CSS 的其他领域(如布局)一直是持续的痛苦来源。

这篇文章讨论的是我在大约十年前遇到的一个问题,直到最近,我一直不知道如何以一种智能的方式解决它。具体来说,它讲述了我是如何使用现代 CSS 网格技术找到一个长期问题的解决方案的,在这个过程中,我得到了比我最初想象的还要酷炫的结果。

请注意,这不是关于如何最佳使用 CSS 网格的教程,而更像是我自己的学习过程的回顾。

问题

我最开始在那个博客上发布的东西之一是来自城市的随机照片,所以我想出了一个想法,要创建一个固定大小的缩略图网格。为了获得更好的外观,我希望这个网格相对于其上下的段落居中对齐,但同时,我希望最后一行上的缩略图相对于网格左对齐。同时,文章的宽度(以及其中网格的宽度)将取决于视窗。

HTML 代码如下所示

<section class='post__content'>
  <p><!-- some text --></p>
  <div class='grid--thumbs'>
    <a href='full-size-image.jpg'>
      <img src='thumb-image.jpg' alt='image description'/>
    </a>
    <!-- more such thumbnails -->
  </div>
  <p><!-- some more text --></p>
</section>

它看起来很简单,但事实证明,这是我遇到的最困难的 CSS 问题之一。

不太理想的解决方案

这些是我尝试过的或多年来看到别人建议过的东西,但从未真正奏效。

浮动不可能

浮动结果是一条死胡同,因为我不知道如何用这种方式使网格居中对齐。

.grid--thumbs { overflow: hidden; }

.grid--thumbs a { float: left; }

下面的演示展示了浮动尝试。调整嵌入大小以查看它们在不同视窗宽度下的行为。

inline-block 疯狂

一开始,这似乎是一个更好的主意

.grid--thumbs { text-align: center }

.grid--thumbs a { display: inline-block }

但事实并非如此

在这种情况下,最后一行没有左对齐。

在某个时候,由于 CodePen 上的意外 CSS 自动完成功能,我发现了名为 text-align-last 的属性,它决定了块的最后一行如何对齐。

不幸的是,在网格上设置 text-align-last: left 也不是我想要的解决方案

在这一点上,我实际上考虑放弃居中对齐网格的想法。text-align: justifiedtext-align-last: left 的组合在网格上会产生更好的效果吗?

嗯,事实并非如此。也就是说,除非最后一行只有一张缩略图,并且列之间的间隙不太大。调整下面的嵌入大小以了解我的意思。

这基本上是我两年前所处的位置,经过九年的尝试和失败,我仍然没有找到这个问题的解决方案。

凌乱的 flexbox 技巧

一个一开始看起来可行的 flexbox 解决方案是在网格上添加一个 ::after 伪元素,并在缩略图和这个伪元素上设置 flex: 1

.grid--thumbs {
  display: flex;
  flex-wrap: wrap;
	
  a, &::after { flex: 1; }
	
  img { margin: auto; }
	
  &:after { content: 'AFTER'; }
}

下面的演示展示了这种方法的工作原理。我已经给缩略图和 ::after 伪元素添加了紫色轮廓,以便更容易看到正在发生的事情。

这不太是我想要的,因为缩略图网格没有居中对齐。也就是说,它看起来并不太糟糕……只要最后一行比其他行少一个图像。但是,一旦发生改变,如果缺少更多项目或没有项目,布局就会失效。

Screenshot collage. Shows how the layout breaks when the last row is not missing exactly one item to be full.
为什么 ::after 技巧不可靠。

这是一个非常规的想法。另一个不是使用伪元素,而是根据我们要期望具有的列数,在缩略图之后添加相同数量的空 div。

我们可以估计出期望的列数,因为缩略图的大小是固定的,并且我们可能希望设置文章的最大宽度,因为跨越整个屏幕宽度的文本在视觉上难以阅读。在这种情况下,用缩略图的固定宽度除以最大宽度应该会得到最大列数。

第一个空元素将占据未完全填充缩略图的行上的全部宽度,而其余元素将溢出到其他行。但由于它们的 height 为零,因此在视觉上无关紧要。

这有点像完成了任务,但同样,它非常规,并且仍然没有产生我想要的确切结果,因为它有时会在列之间留下很大且看起来很丑陋的间隙。

网格解决方案?

鉴于它的名称,网格布局一直听起来像答案。问题是,到目前为止,我看到的所有示例都使用了预定义的列数,而这对于这种特定模式不起作用,因为列数由视窗宽度决定。

去年,在编写一系列包含一个元素的纯 CSS 背景模式时,我想到了生成大量媒体查询来修改 CSS 变量 --n 的想法,该变量对应于用于设置 grid-template-columns 的列数。

$w: 13em;
$h: 19em;
$f: $h/$w;
$n: 7;
$g: 1em;

--h: #{$f*$w};
display: grid;
grid-template-columns: repeat(var(--n, #{$n}), var(--w, #{$w}));
grid-gap: $g;
place-content: center;
	
@for $i from 1 to $n {
  @media (max-width: ($n - $i + 1)*$w + ($n - $i + 2)*$g) {
    --n: #{$n - $i}
  }
}

实际上,当时我对这个想法非常自豪,即使现在回过头来看,我仍然感到有点尴尬。对每个可能的列数都创建一个媒体查询并不理想,更不用说它在网格宽度不等于视窗宽度时效果不太好,但它仍然有点灵活,并且还取决于其同级元素的宽度。

神奇的解决方案

在使用 CSS 网格并无法理解为什么 repeat() 函数在特定情况下不起作用时,我终于找到了一种更好的解决方案。这非常令人沮丧,促使我去 MDN 查找,在那里我偶然发现了 auto-fit 关键字,虽然我不理解解释,但我有一种预感,它可以帮助解决这个其他问题,因此我放弃了正在做的一切,试了一下。

以下是我的结果

.grid--thumbs {
  display: grid;
  justify-content: center;
  grid-gap: .25em;
  grid-template-columns: repeat(auto-fit, 8em);
}

我还发现了 minmax() 函数,它可以在网格项目上固定大小的地方使用。我仍然无法完全理解 minmax() 的工作原理——我玩弄它的时间越多,就越不理解它——但它在这种情况下看起来做的事情是创建网格,然后均匀拉伸其列,直到它们填充所有可用空间

grid-template-columns: repeat(auto-fit, minmax(8em, 1fr));

我们在这里可以做的另一件很酷的事情是防止图像溢出,即使它比网格元素更宽。我们可以用 min(8em, 100%) 替换最小 8em 来做到这一点。这基本上确保了图像永远不会超过 100%,但永远不会低于 8em。感谢 Chris 的建议!

请注意,min() 函数在预 Chromium Edge 中不起作用!

请记住,只有当所有图像具有相同的纵横比时,此方法才能产生理想的结果——就像我在这里使用的正方形图像一样。对于我的博客,这不是问题,因为所有照片都是用我的索尼爱立信 W800i 手机拍摄的,它们都具有相同的纵横比。但是,如果我们放入具有不同纵横比的图像,网格将不再那么美观。

当然,我们可以将图像的 `height` 设置为固定值,但这会扭曲图像……除非我们将 object-fit 设置为 `cover`,这将解决我们的问题!

另一个想法是将第一个缩略图变成一种横幅,跨越所有网格列。唯一的问题是我们不知道列数,因为这取决于视窗。但是,有一个解决方案——我们可以将 `grid-column-end` 设置为 `-1`!

.grid--thumbs {
  /* same styles as before */
	
  a:first-child {
    grid-column: 1/ -1;
		
    img { height: 13em }
  }
}

第一张图片比其他所有图片都具有更大的 `height`。

当然,如果我们希望图像跨越除最后一列以外的所有列,我们可以将其设置为 `-2`,依此类推……负列索引是有效的!

auto-fill 是我在 MDN 上注意到的另一个网格属性关键字。它们的解释都是没有视觉效果的长篇文字,因此我发现它们并不特别有用。更糟糕的是,在上面任何网格演示中将 `auto-fit` 替换为 `auto-fill` 没有任何区别。即使查看了 文章 或玩弄 示例,它们到底是如何工作的以及它们之间的区别仍然是一个谜。

然而,尝试不同的方法并在各种情况下查看结果,让我得出一个结论,如果我们使用的是 `minmax()` 列宽度而不是固定宽度(比如 `8em`),那么最好使用 `auto-fill` 而不是 `auto-fit`,因为如果我们碰巧只有几张图片,结果看起来会更好,如下面的交互式演示所示。

我认为我最喜欢的是最初的想法,即一个居中对齐且列宽基本固定的缩略图网格(但仍然使用 `min(100%, 15em)` 而不是仅仅 `15em`)。归根结底,这取决于个人喜好,下面的演示中所看到的内容恰好对我来说看起来更好。

我在这个演示中使用的是 `auto-fit`,因为它产生与 `auto-fill` 相同的结果,并且字符更少。但是,在制作这个演示时,我并不理解为什么这两个关键字会产生相同的结果,因为图库中的项目比我们填充一行所需的更多。

但一旦改变,`auto-fit` 和 `auto-fill` 就会产生不同的结果,如下所示。您可以更改 `justify-content` 值和放置在网格上的项目数量。

我并不确定哪个是更好的选择。我想这也要取决于个人喜好。与 `justify-content: center` 结合使用,`auto-fill` 似乎是更合乎逻辑的选择,但同时,`auto-fit` 却产生了更美观的结果。