使用CSS网格和Flexbox的容器查询解决方案(附代码示例)

199 阅读12分钟

真正的容器查询是一个被广泛要求的CSS功能,它是对媒体查询的补充,但被置于容器元素而不是视口。

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

我们将涵盖:

  • ⏸ 使用CSS网格布局和flexbox来实现从等宽列到行布局的容器依赖性调整
  • 🏆 解决可变宽度断点列的 "圣杯 "方案
  • 🚀 实施CSS自定义变量,使解决方案尽可能具有可扩展性

对问题的理解

当Ethan Marcotte引入响应式设计的概念时,我们开始接受培训,通过使用媒体查询来调整页面上的元素,使其跨越视口尺寸。

我是Ethan的超级粉丝,记得在文章发表后的几周内就读过那篇文章。它很容易成为网络上最具变革性的文章之一。

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

但仍有一个领域,CSS并没有完全的答案:你如何响应页面上单个容器内的变化,而不是整个视口?

这就产生了12列网格和相关的框架,如Bootstrap,作为一种中间解决方案,在不编写不断增加的一次性媒体查询墙的情况下,即时应用宽度调整。

而这些解决了很多常见的挫折,并几乎让我们达到了目的。

作为一个营销网站开发的老手,依赖网格框架的最大缺点是仍然被束缚在工具上。我们很少刻意在12开的网格之外进行设计,因为开发额外的样式和处理自定义媒体查询的开销很大。

<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 列在视口缩小之前不会变成全宽。这意味着,嵌套的列可能非常狭窄。为了解决这个问题,你可能不得不添加一堆额外的实用类来切换,切换,甚至可能再次切换。

如果这些列能以某种方式意识到它们的内容,并根据最小的内容宽度而不是依赖视口宽度来中断,那不是很好吗?

欢迎对容器查询的需求🙌。

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

解决方案

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

预览

为了进一步提供我们要实现的目标的背景,这里是最终的结果。

demo of dashboard with container queries

只用了三个类和三个CSSvars() --grid和flexbox解决方案各一个--我们就使 "卡片 "从1-3跨度响应式切换*,*卡片内容从列到行的布局切换,而且没有媒体查询。

网格解决方案

我们将首先创建.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 ,以防止计算值随字体变化而变化。font-size 然而,在演示中,这个值在技术上确实使用了应用于body ,如果你没有改变它,它就相当于1rem 。这是因为它被放在ul ,而不是排版元素上,所以ul 默认从body 继承font 的属性。

你唯一应该使用的单元是% ,因为那会阻止列的折叠。

酷,这就解决了 "卡 "的问题。

demo of responsive grids columns

网格容器内容

现在要处理的是卡片内容,如果空间允许的话,我们也希望能在列中显示。但是,这个 "断点 "应该更小。

一开始,你可能会接触到一个实用类。事实上,我也这么做了。

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

首先,我们要设置我们的初始变量值,它被分配到:root 。我们要设置的值是minmaxmin 部分:

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

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

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

然后我们可以将我们的.grid 类添加到li ,这是我们的 "卡片 "容器,然后使用内联样式来修改--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;

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

然而,这永远不可能被打破,因为grid中没有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 Albatross"。

让我们开始我们的规则:

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

这里值得注意的是确保flex-wrap: wrap ,否则 "断点 "效果就不会真正发生。

现在,flexbox和grid layout之间的一个很大的区别是,flex children的大小行为并不在父级上设置。

为了使我们的规则最灵活,除了通用扇形--* --之外,我们还将使用儿童组合器--> --来开始一个规则,该规则将应用于任何元素类型的直接flex children。

使用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#

截至发稿时,flexboxgap 属性正在获得支持,但它还不是很可靠的。

我们将调整我们的规则,暂时使用margin作为一个polyfill。

你知道吗: margin不会在flexbox或grid的子节点上折叠,所以任何提供的值都会在子节点之间复合。

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

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

这些规则在每个子节点周围添加.5rem ,其外部部分被.flex 父节点上的负边距所否定。

调整断点

与网格不同,这个基础解决方案意味着所有的列将在同一时间 "断裂"。

demo of flexbox albatross

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

让我们添加变量并更新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>

Annndddd - 在调整大小时,等一下 - 所有三张卡同时断裂,只是比以前早了一些 🤔

发生了什么事?

flex-basis: 1 +物品的数量是有原因的。

一旦中间的牌掉下来,其他两张牌就会全幅展开,这要感谢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 children的内容周围添加一个嵌套包装:

<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>

这基本上是重置了上下文,所以它不能影响到父容器。与grid相比,这是一个小麻烦,但能实现几乎相同的功能。

demo of flexbox content container queries

Flexbox解决方案#2#

如果你不需要为Flexbox项目设置多个独特的断点,我们可以使用Una命名的"Deconstructed Pancake"来统一应用断点,只需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 children上独立设置
  • 当内容超过基于容器的可用水平宽度时,"包裹 "会被固有地触发。

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

> * {
  // ...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 的柔性子集。如果需要,我们可以把它移到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 的最小宽度时,每个列表项是如何有独立的包装行为。

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