使用容器查询构建“智能”布局

Avatar of Kevin Powell
Kevin Powell

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

现代 CSS 不断为我们提供许多新的、更轻松的方式来解决旧问题,但我们获得的新功能通常不仅解决了旧问题,而且还开辟了新的可能性。

容器查询 就是其中一项开辟新可能性的功能,但由于它们看起来与使用媒体查询的旧方法非常相似,我们的第一直觉就是以相同的方式使用它们,或者至少以非常相似的方式使用它们。

然而,这样做并不能充分利用容器查询与媒体查询相比的“智能”之处!

由于 媒体查询 对响应式网页设计的到来至关重要,我不想对它们说任何不好的话...... 但是媒体查询很愚蠢。 并不是说这个概念愚蠢,而是说它们知道的不多。 事实上,大多数人认为它们知道的比实际知道的更多。

让我们使用这个简单的示例来阐述我的意思

html {
  font-size: 32px;
}

body {
  background: lightsalmon;
}

@media (min-width: 35rem) {
  body {
    background: lightseagreen;
  }
}

为了改变背景颜色,视窗尺寸应该多大? 如果您说的是 1120px 宽 - 对于那些没有费心进行计算的人来说,这是 35 乘以 32 的结果 - 您并非孤军奋战,但您也错了。

还记得我曾经说过媒体查询知道的不多吗? 它们只知道两件事

  • 视窗的大小,以及
  • 浏览器的字体大小。

当我说 浏览器的字体大小 时,我指的是文档中的根字体大小,而不是上面的示例中 1120px 错误的原因。

它们查看的字体大小是在应用任何值(包括用户代理样式)之前来自浏览器的初始字体大小。 默认情况下,该大小为 16px,不过用户可以在浏览器设置中更改该值。

是的,这是故意的媒体查询规范 指出

媒体查询中的相对长度单位基于初始值,这意味着单位绝不基于声明的结果。

这似乎是一个奇怪的决定,但如果没有这样,如果我们这样做会发生什么呢

html {
  font-size: 16px;
}

@media (min-width: 30rem) {
  html {
    font-size: 32px;
  }
}

如果媒体查询查看根 font-size(就像大多数人假设的那样),当视窗宽度达到 480px 时,您会遇到循环,font-size 会不断增大,然后又不断减小。

容器查询更聪明

虽然媒体查询有这个限制,而且有充分的理由,但容器查询不必担心这类问题,这开辟了许多有趣的可能性!

例如,假设我们有一个网格,它应该在较小尺寸下堆叠,但在较大尺寸下应该是三列。 使用媒体查询,我们必须 通过魔法数字 找出发生这种情况的确切点。 使用容器查询,我们可以确定我们希望每列的最小尺寸,它将始终有效,因为我们查看的是容器的大小。

这意味着我们不需要用于断点的魔法数字。 如果我想要三列,每列的最小尺寸为 300px,我知道当容器宽度为 900px 时我可以拥有三列。 如果我用媒体查询来做这件事,它将不起作用,因为当我的视窗宽度为 900px 时,我的容器通常小于该宽度。

但更棒的是,我们也可以使用任何我们想要的单位,因为容器查询与媒体查询不同,它可以查看容器本身的字体大小。

对我来说,ch非常适合这种事情。 使用 ch,我可以说“当我拥有足够的空间让每列的最小宽度为 30 个字符时,我想要三列。”

我们可以像这样自己进行计算

.grid-parent { container-type: inline-size; }

.grid {
  display: grid;
  gap: 1rem;

  @container (width > 90ch) {
    grid-template-columns: repeat(3, 1fr);
  }
}

而且这确实非常有效,正如您在这个示例中看到的。

作为另一个奖励,感谢 Miriam Suzanne,我最近了解到 您可以在媒体查询和容器查询中包含 calc(),因此,与其自己进行计算,您可以像这样包含它:@container (width > calc(30ch * 3)),正如您在这个示例中看到的

更实际的用例

使用容器查询的一件令人讨厌的事情是 必须有一个定义好的容器。 容器不能查询自身,因此我们需要在要使用容器查询选择的元素上方添加一个额外的包装器。 您可以在上面的示例中看到,我需要在网格的外部添加一个容器才能使它正常工作。

更令人讨厌的是,当您希望网格或弹性盒子子元素根据它们所占的空间大小来改变布局时,却意识到如果父元素是容器,这实际上不起作用。 我们最终不得不 将每个网格或弹性盒子项目包装在一个容器中,就像这样

<div class="grid">
  <div class="card-container">
    <div class="card">
  </div>
  <div class="card-container">
    <div class="card">
  </div>
  <div class="card-container">
    <div class="card">
  </div>
</div>
.card-container { container-type: inline-size; }

总的来说,这并没有那么糟糕,但确实有点烦人。

但是,有一些解决方法!

例如,如果您使用的是 repeat(auto-fit, ...),您 可以 使用主网格作为容器!

.grid-auto-fit {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(30ch, 100%)), 1fr);
  container-type: inline-size;
}

知道每列的最小尺寸是 30ch,我们可以利用这些信息根据我们拥有的列数来重新设置单个网格项目的样式

/* 2 columns + gap */
@container (width > calc(30ch * 2 + 1rem)) { ... }

/* 3 columns + gaps */
@container (width > calc(30ch * 3 + 2rem)) { ... }

我在这个示例中使用了它,根据我们有一列、两列还是三列,来更改网格中第一个子元素的样式。

虽然更改某些东西的背景颜色对于演示来说很棒,但我们当然可以对此做更多的事情

这种方法的缺点

我发现使用这种方法的唯一缺点是,我们不能对断点使用自定义属性,这将真正改善这种方法的开发体验。

考虑到 媒体查询第 5 级规范 的规范编辑器草案中包含自定义媒体查询,这一点应该 最终 会改变,但它已经存在了一段时间,并且没有任何浏览器有任何进展,因此我们可能还需要很长时间才能使用它们。

虽然我认为使用自定义属性来实现这些操作可以提高可读性和更新效率,但即使没有它们,它也开辟了足够多的可能性,因此仍然值得使用。

弹性盒子怎么样?

对于弹性盒子,弹性盒子项目定义了布局,因此这有点奇怪,因为我们对项目应用的尺寸在断点中很重要。

可以 正常工作,但是 如果您使用弹性盒子进行此操作,可能会出现一个重大问题。 在我们查看这个问题之前,这里有一个关于如何使用弹性盒子使它正常工作的简短示例

.flex-container {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;

  container-type: inline-size;
}

.flex-container > * {
  /* full-width at small sizes */
  flex-basis: 100%;
  flex-grow: 1;

  /* when there is room for 3 columns including gap */
  @container (width > calc(200px * 3 + 2rem)) {
    flex-basis: calc(200px);
  }
}

在本例中,我使用了 px 来展示它也能正常工作,但您可以像网格示例那样使用任何单位。

这看起来像是您可以使用媒体查询实现的功能 - 您也可以在其中使用 calc()! - 但这只有在父元素的宽度与视窗宽度匹配的情况下才有效,而这种情况大多数情况下不会发生。

如果弹性盒子项目有填充,这将失效

许多人没有意识到这一点,但 弹性盒子算法不会考虑填充或边框,即使您更改了 box-sizing。 如果您的弹性盒子项目有 padding,您基本上必须通过魔法数字来实现它。

这是一个示例,我在其中添加了一些填充,但没有更改其他任何内容,您会注意到其中一个尴尬的两列布局(其中一列在底部拉伸)。

因此,我通常发现自己更频繁地使用这种方法,而不是 flexbox,但肯定存在仍然可以使用它的情况。

和之前一样,因为我们知道有多少列,我们可以利用这一点来创建更动态和更有趣的布局,具体取决于给定元素或元素的可用空间。

打开一些有趣可能性

我刚开始尝试这种方式,我发现它开辟了一些我们之前在媒体查询中从未有过的可能性,这让我很兴奋地想知道还能实现什么!