真正的容器查询是一个被广泛要求的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处理容器查询,并讨论各自的优缺点。
预览
为了进一步提供我们要实现的目标的背景,这里是最终的结果。

只用了三个类和三个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 的属性。
你唯一不应该使用的单元是% ,因为那会阻止列的折叠。
酷,这就解决了 "卡 "的问题。

网格容器内容
现在要处理的是卡片内容,如果空间允许的话,我们也希望能在列中显示。但是,这个 "断点 "应该更小。
一开始,你可能会接触到一个实用类。事实上,我也这么做了。
但现代CSS给了我们一个更灵活的方法:自定义变量。
首先,我们要设置我们的初始变量值,它被分配到:root 。我们要设置的值是minmax 的min 部分:
: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>
形成我们最终的网格解决方案。请注意,当卡片容器变窄或变宽时,卡片内容是如何从列到行独立调整的。


为什么网格不能做可变宽度的列?
是什么阻碍了这个解决方案处理可变宽度的列,同时保持 "容器查询 "的好处,不需要媒体查询就能打破行的布局?
我们可以用最简单的方法来实现网格的可变宽度列,那就是去掉我们之前的工作,而使用:
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 父节点上的负边距所否定。
调整断点
与网格不同,这个基础解决方案意味着所有的列将在同一时间 "断裂"。

也就是说,直到我们添加我们的朋友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 调整到第一张或_最后一张卡片上,那么其余两张卡片就会保持较小的 "断点"。


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相比,这是一个小麻烦,但能实现几乎相同的功能。

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 ,在这一点上,项目将开始下降到新的行中。这使得行为类似于网格解决方案*,只是*被丢弃的项目将扩大以填补可用的区域。
在大多数情况下,这个解决方案已经成为我的首选。

圣杯:可变宽度的断点栏
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>
注意这也适用于解构煎饼的方法。
这就是结果:

你可能觉得这个例子有点刺耳,但它说明了一旦非自动项目由于容器宽度的缩小而达到8rem 的最小宽度时,每个列表项是如何有独立的包装行为。
更实际的是,这也适用于演示中的图标+标题锁定。该演示还显示了使用信天翁行为的同一个列表,以提供一种方法来比较这些方法。