容器查询方案之 CSS Grid 和 Flexbox 【译】

avatar

All TutorialsMTopics List

这是系列中的第13集,探讨了Stephanie Eckles在过去15年多的前端开发中一直在解决的问题的现代CSS解决方案。

真正的容器查询是一个非常需要的CSS功能,它将是媒体查询的补充,但被放置在容器元素而不是视口上。

实验性的CSS容器查询在这里!这是 Miriam Suzanne 的容器查询建议解释器,它在Chrome Canary中有一个实验原型。下载Canary后,请访问 chrome://flags 搜索并启用容器查询。有关更多信息,请查看我的 容器查询入门

使用grid和flexbox,我们可以创建响应容器和内容宽度的样式,并克服容器查询要解决的一些痛点。

我们将涵盖:

  • ⏸ 使用CSS网格布局和flexbox实现从等宽列到行布局的容器相关调整
  • 🏆 可变宽度断点列的“圣杯”解决方案
  • 🚀 实现CSS自定义变量,使解决方案尽可能可扩展

理解问题

当Ethan Marcotte引入 响应式设计 的概念时,我们开始通过使用媒体查询来调整页面上的元素。

我是Ethan的超级粉丝,记得在发表后的几周内阅读了那篇文章。这无疑是网络上最具变革性的文章之一。

P.S. - 响应式设计最近已经10岁了!阅读Ethan的 评论

十年前,媒体的询问是有意义的,因为我们总是被当时可用的工具所束缚。

但是还有一个地方CSS没有完全回答:你如何响应页面上单个容器而不是整个视口中的更改?

这就产生了12列网格和相关的框架,如Bootstrap作为一个中间解决方案,在运行中应用宽度调整,而无需编写一次性媒体查询的每一个增长的墙壁。

这些解决了很多常见的问题,我们差不多可以应付绝大多数场景。

作为营销网站开发的老手,依赖网格框架的最大缺点仍然是被绑定到工具上。我们 很少 会有意地设计12列网格之外的内容,因为开发额外的样式和处理自定义媒体查询的开销很大。

让我们看一个 Bootstrap docs的例子

<div class="container">
  <div class="row">
    <div class="col-sm">One of three columns</div>
    <div class="col-sm">One of three columns</div>
    <div class="col-sm">One of three columns</div>
  </div>
</div>

这里的关键是 col-sm 类,它们需要从设置的“小”视口宽度(Bootstrap为540px)上方的“等宽列”切换到更小视口上的全宽。

但是,如果这些列在另一列中呢?

这就是我们遇到问题的地方:在视口大小减小之前, col-sm 列不会变成全宽。这意味着,嵌套列可能非常窄。要解决这个问题,您可能必须添加一堆额外的class类来不停的切换 class 。

如果这些列以某种方式知道它们的内容,并基于最小内容宽度而不是依赖于视口宽度来中断,那不是很好吗?

终于来到需要容器查询的场景了 🙌

注意:不幸的是,针对grid与flexbox的类似解决方案被限制为相等的列宽,但这仍然有助于减少对媒体查询的需求,并填补了基于容器与视口宽度的行为的差距。跳到本页可变宽度列的“圣杯”解决方案。

解决方案

我们将看看如何使用grid和flexbox处理容器查询,并讨论各自的优缺点。

预览

为了进一步提供我们希望实现的内容,这里是最终结果:

demo of dashboard with container queries

仅使用三个类和三个CSS vars() -网格和每个flexbox解决方案各一个-我们已经使“卡片”响应地从1-3切换,卡片内容从列切换到行布局,没有媒体查询。😎

网格解决方案

CSS网格布局新手?我们将重新讨论一种方法,我在替换12列网格的解决方案 中也描述过,并且在我的两个与响应式网格相关的 视频 中进行了探索。

我们将开始创建 .grid 类,设置显示,并添加一个适度的间隙值:

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

之后,我们只需要一行代码来启动容器查询:

grid-template-columns: repeat(auto-fit, minmax(20ch, 1fr));

repeat 函数将定义的列行为应用于所有存在的列。这使得它可以扩展到可以从任何类型的内容创建的任何数量的列。

然后,我们使用 auto-fit 值而不是绝对数字,该值负责通过拉伸列以填充任何可用空间来确保列保持等宽。

之后,我们使用 minmax() 设置允许的最小列宽,然后使用 1fr 作为最大值,以确保内容尽可能多地填充列。

minmax() ,特别是 20ch ,我们基本上已经定义了媒体查询域中的“边界值”。

ch 单位等于当前字体的 0 (零)字符,这使得它对当前内容格外敏感。

你当然可以切换到 rem ,以防止计算值随着字体的变化而变化。然而,在演示中,这个值在技术上确实使用了应用于 body 的 font-size ,如果你没有改变它,它将等同于 1rem 。这是因为它被放置在 ul 上,而不是排版元素上,所以 ul 默认继承了 body 的 font 属性。

唯一不应该使用的单位是 % ,因为这会阻止列合并。

很酷,这可以解决“cards”问题:

demo of responsive grids columns

网格容器内容

现在要处理卡片内容,如果空间允许,我们还希望允许在列中显示这些内容。但是,“边界值”应该更小。

首先,您可能需要一个实用程序类。事实上,我也是。

但是现代CSS给了我们一种更灵活的方式:自定义变量。

「自定义变量」如何做呢? (译者添加)

首先,我们必须设置初始变量值,它被分配给 :root 。我们设置的值是 minmax 的 min 部分:

:root {
  --grid-min: 20ch;
}

然后相应地更新我们的规则:

grid-template-columns: repeat(auto-fit, minmax(var(--grid-min), 1fr));

然后,我们可以将 .grid 类添加到 li 容器中,这是我们的“card”容器,然后使用inline style修改 --grid-min 值:

<li class="card grid" style="--grid-min: 15ch">
  <p>Jujubes soufflé cake tootsie roll sesame snaps cheesecake bonbon.</p>
  <p>
    Halvah bear claw cheesecake. Icing lemon drops chupa chups pudding tiramisu.
  </p>
</li>

最终的网格解决方案。请注意,当卡片容器变窄或变宽时,卡片内容如何从列到行独立调整布局。

demo of final grid container query solution

woman crying and saying "it's so beautiful"

为什么网格不能做可变宽度列?

是什么阻碍了这个解决方案在处理可变宽度列的同时保持“容器查询”的好处,即在没有媒体查询的情况下打破行布局?

我们可以用最简单的方法来获得可变宽度列的网格,这就是删除我们以前的工作,而是使用用途:

grid-auto-flow: column;

它翻转了默认轴,默认情况下创建的列确实是可变宽度的。

然而,这永远不会崩溃,因为网格中没有 wrap 属性。这意味着您可能会遇到溢出,这对于可预测的短内容是可以接受的。它也可以被放置在媒体查询中,以仅在特定视口宽度以上触发该行为-这再次与“容器查询”的目标相反。

我非常希望我们的解决方案可以扩展到这个用例:

style="--grid-min: min-content;"

这将计算更新我们的属性定义为:

grid-template-columns: repeat(auto-fit, minmax(min-content, 1fr));

从理论上讲,这似乎是一个近乎完美的解决方案。通常, min-content 表示允许内容收缩到容纳内容所需的最小宽度(在文本块中,这基本上意味着收缩到最长的单词)。

不幸的是, repeat 规范明确禁止这种行为:

自动重复(自动填充或自动调整)不能与固有或灵活的大小相结合。

其中 min-content 被认为是“固有尺寸”之一。

继续阅读以了解flexbox如何提供这种行为!

Flexbox解决方案

我要描述的第一个flexbox解决方案是Heydon Pickering创建的一个技术示例,称为“Flexbox”。

让我们开始我们的规则:

.flexbox {
  display: flex;
  flex-wrap: wrap;
}

这里值得注意的是确保设置了 flex-wrap: wrap ,否则“断点”效果实际上永远不会发生。

现在,flexbox和网格布局之间的一个很大的区别是,flex子元素的大小调整行为不是在父元素上设置的。

为了使我们的规则最灵活,我们将使用子组合符- > -以及通用扇区- * -来开始一个规则,该规则将应用于任何元素类型的直接flex子元素。

使用Sass,我们可以将其整齐地嵌套在前面的属性下:

.flexbox {
  // ...existing rules

  > * {
    // flex children rules
  }
}

这里是“Flexbox”魔术发生的地方。让我们添加规则,然后讨论。

> * {
  flex-grow: 1;
  flex-basis: calc((35rem - 100%) * 999);
}

flex-grow: 1 确保列将填充算法和其他属性值允许的尽可能多的空间。

flex-basis 规则对CSS属性 calc 执行了一些数学魔术,本质上导致元素最小值为 35rem ,低于该最小值则扩展为 100% 。

结果是等宽列,直到达到最小可接受宽度。

不幸的是, calc 不允许使用 ch 值,这会使您无法直观地看到列何时断开。在这个演示中,我们发现在给定的字体和大小下, 35rem 几乎等同于 20ch 。

创建 gap

在撰写本文时,flexbox gap 属性正在获得支持,但它还不是很可靠。

我们将调整我们的规则,使用保证金作为polyfill现在。

你知道吗:在flexbox或grid子元素上,边距不会折叠,因此任何提供的值都会在子元素之间复合。

.flex {
  // ...existing styles
  margin: 1rem -0.5rem;

  > * {
    // ...existing styles
    margin: 0.5rem;
  }
}

这些规则在每个子元素周围添加 .5rem ,其外部部分在 .flex 父元素上用负边距求反。

调整断点

与网格不同,这个基本解决方案意味着所有列将同时“中断”:

demo of flexbox albatross

也就是说,直到我们添加我们的朋友CSS变量 ✨

Let's add the variable and update flex-basis:
让我们添加变量并更新 flex-basis :

// Update on `:root`
--flex-min: 35rem;

// Update in `.flexbox`
flex-basis: calc((var(--flex-min) - 100%) * 999);

现在,我们将更新它的中间“卡片”:

<li class="card" style="--flex-min: 50rem;"></li>

-在调整大小,等待一分钟-所有三张卡在同一时间,只是比之前更早🤔

怎么回事?

flex-basis: 1 + the number of items is to blame.
flex-basis: 1 0 flex-basis: 1 0 flex-basis: 1 0 flex-basis: 1 0 flex-basis: 1 0 flex-basis: 1 0 flex-basis: 1 0 flex-basis: 1 0 flex-basis: 1 0 flex-basis: 1 0 flex-basis: 1 0 flex-basis: 1 0 flex-basis: 1 0 flex-basis: 1 0 flex-basis: 1 0 flex-basis: 1 0 flex-basis: 1 0 flex-basis: 1 00 flex-basis: 1 00 flex-basis: 1 0 flex-basis: 1 0 flex-basis: 1 00 flex-basis: 1 0 flex-basis: 1 0 flex-basis: 1 0 flex-basis: 1 00 flex-basis: 1 00 flex-basis: 1 0 flex-basis: 1 0 flex-basis: 1 00 flex-basis: 1 000 flex-basis: 1 000 flex-basis: 1 000 flex-basis: 1 flex-basis: 1 00000# flex-basis: 1 0000 flex-basis: 1 0000 flex-basis: 1 flex-basis: 1 0000000000 flex-basis: 1 0000

一旦中间的卡片掉落,其他两张卡片会因为 flex-basis: 1 而展开全宽。

如果我们将 --flex-min 调整移动到第一张或_最后一张卡,那么剩下的两张卡将保持较小的“断点”。

demo of last card breaking at the adjusted min width

little girl rolling her eyes and waving her hands in a "whatever" gesture

Flexbox容器内容

好的,现在让我们解决从列布局切换到行布局的段落内容的相同想法。

由于 --flex-min 变量已经就位,我们已经拥有了所需的内容。

然而,对于我们刚刚经历的“陷阱”,我们需要在Flex子内容周围添加一个嵌套包装器:

<div class="flex" style="--flex-min: 18rem;">
  <p>Jujubes soufflé cake tootsie roll sesame snaps cheesecake bonbon.</p>
  <p>
    Halvah bear claw cheesecake. Icing lemon drops chupa chups pudding tiramisu.
  </p>
</div>

这实际上重置了上下文,因此它不会影响父容器。与网格相比,这是一个小麻烦,但实现了几乎相同的功能:

demo of flexbox content container queries

Flexbox解决方案

如果你不需要为flexbox项目设置多个唯一的断点,我们可以使用Una命名的“解构”来统一应用断点,只使用 flex-basis 。

该技术优于Flexbox Albatross的优点:

  • flex-basis 断点基于单个项,而不是分配给父项的宽度
  • 项将彼此独立地开始新行,而不是一次全部开始,而无需定义唯一的断点
  • 我们可以使用 ch ,因为不涉及 calc

一个潜在的缺点是需要嵌套元素来解决宽度自定义变量( --pancake-min )设置之间的冲突,如演示中所示,当尝试为卡片段落内容设置新断点时。

以下是基本的CSS:

:root {
  --pancake-min: 20ch;
}

.flex-pancake {
  display: flex;
  flex-wrap: wrap;
  margin: 1rem -0.5rem;

  > * {
    flex: 1 1 var(--pancake-min);
    margin: 0.5rem;
  }
}

这看起来类似于我们如何设置Flexbox Albatross解决方案,直到我们到达子节点的 flex 规则。

包含计算变量的简写为:

flex-grow: 1;
flex-shrink: 1;
flex-basis: 20ch;

我们允许项目增长和收缩,直到区域太窄,无法容纳所有基于 flex-basis 的项目,此时项目将开始下降到新行。这使得行为类似于网格解决方案,除了拖放的项将扩展以填充可用区域。

在大多数情况下,这种解决方案已成为我的首选。

demo of the deconstructed pancake technique

圣杯:可变宽度断点列

Flexbox将允许我们创建一个方法来指定某些列应该收缩到它们的“自动”宽度,而其他列则具有独立的“最小宽度”行为。这将导致可变宽度列最终保留基于容器宽度的“断点”行为。

使flexbox成为解决方案的两个关键:

  • “列”宽度可以在Flex子项上独立设置
  • 当内容超过基于容器的可用水平宽度时,

我们将创建一个实用程序类来分配“自动宽度”行为:

> * {
  // ...existing styles

  &.flex--auto {
    flex: 0 1 auto;
  }
}

该简写计算为:

flex-grow: 0;
flex-shrink: 1;
flex-basis: auto;

这导致了只增长到网格认为的 max-content 的行为,并被允许无限期地收缩。

与其他使用flexbox行为的flex子元素相结合, .flex--auto 项将在其兄弟元素实际上从其允许的最小宽度中断时出现中断行为。

为了说明这一点,我们可以在现有的“卡”中设置以下内容。注意 --flex-min 值的更新。这可以根据呈现的内容进行调整以适应口味,并且它仅适用于没有 .flex--auto 的Flex儿童。如果需要,我们可以将其应用于 li 中的 span ,以进行更专门的调整。

<ul class="list-unstyled" style="--flex-min: 8rem;">
  <li class="flex">
    <strong class="flex--auto">Ice Cream</strong> <span>Butter Pecan</span>
  </li>
  <li class="flex">
    <strong class="flex--auto">Musical Artist</strong>
    <span>Justin Timberlake</span>
  </li>
  <li class="flex">
    <strong class="flex--auto">Painter</strong> <span>Vincent Van Gogh</span>
  </li>
</ul>

请注意,这也适用于解构方法。*

结果是这样的:

demo of holy grail behavior

你可能会觉得这个例子有点不和谐,但是它说明了当非自动项由于容器宽度收缩而达到 8rem 最小宽度时,每个列表项都有独立的包装行为。

更实际的是,这也适用于演示中的图标+标题锁定。该演示还使用信天翁行为显示了相同的列表,以提供一种比较方法的方法。

Demo 演示

我鼓励你在CodePen中打开它,以便能够操纵视口大小。

顶部行(绿色轮廓)使用网格解决方案,中间行(红色轮廓)使用Flexbox Albatross解决方案,底部行(紫色轮廓)使用Deconstructed Pancake解决方案。每个flexbox解决方案的第二个卡片列表展示了每个列表项的“圣杯”解决方案。

何时使用每种方法

选择 grid ,如果:

  • 具有更“读者友好”的最小宽度设置的等宽列是优先考虑的( ch vs. rem )
  • 当列达到可接受的最小宽度时,更希望它们更加独立地断开
  • 奇数列的“孤立”列是可以接受的(在演示中的中等大小视口中可见)

选择 flex ,如果:

  • 您需要可变宽度的列,这些列仍然具有基于容器大小的“断点”行为
  • gap 支持不足不是问题

选择Flexbox,如果:

  • 可以接受列同时命中断点
  • 断点调整的后果是可接受的(额外列可能会按所述断开为行)
  • 您希望 flex-basis 断点是父级的宽度

选择解构,如果:

  • 您需要基于项宽度的 flex-basis 断点
  • 您希望项逐个拆分为新行
  • 你想使用像 ch 这样的CSS单元,这在 calc 中是不允许的。

下一步读什么

浏览整个系列

容器查询单位和流体排版


原文链接