你拿到一个看起来不错的设计,它有一个非常棒的英雄区,下面紧跟着一个三列布局。你知道,就像你以前做过的几乎所有其他网站一样。

你快速完成了英雄区,开始着手制作三列布局。现在是拿出我们可靠的朋友 Flexbox 的时候了!然而,你写下了 display: flex
,结果却得到了一团糟,而不是你期望的三列等宽布局。
出现这种情况是因为 Flexbox 计算元素基本大小的方式。你可能已经阅读过许多 Flexbox 教程,其中许多教程(包括我自己写的)都过于简单地介绍了 Flexbox 的功能,而没有真正深入探讨 Flexbox 为我们完成的那些我们认为理所当然的复杂操作。
我相信你已经看到过类似这样的例子和教程,其中三个 div 围绕里面的内容缩小
换句话说,我们看到一些块级元素缩小并并排排列。感觉上 Flexbox 希望元素尽可能小。但实际上,Flexbox 实际上希望元素尽可能大。
等等,什么?Flexbox 默认情况下会缩小元素——这不可能吧!对吧?
尽管 Flexbox 非常棒,但它在幕后进行的操作实际上有点奇怪,因为默认情况下,它同时执行两个操作。首先,它会查看内容大小,即我们在元素上声明 width: max-content
时会得到的大小。但除此之外,flex-shrink
也会发挥作用,允许元素变小,但前提是需要时才变小。
为了真正理解正在发生的事情,让我们将这两个方面分解开来,看看它们是如何协同工作的。
max-content
深入 max-content
在某些情况下是一个非常方便的属性值,但我们需要了解它在这个特定情况下是如何工作的,我们试图获得三个看似简单的等宽列。所以让我们暂时忽略 Flexbox,看看 max-content
本身是如何工作的。
MDN 对此进行了很好的 解释
max-content
大小关键词表示内容的内在最大宽度。对于文本内容,这意味着即使会导致溢出,内容也不会换行。
内在可能让你感到困惑,但它基本上意味着我们让内容决定宽度,而不是我们显式设置一个固定的宽度。Uri Shaked 恰当地描述了这种行为,他说:“浏览器假装它有无限的空间,并在测量宽度时将所有文本放在一行中。”
所以,底线是,max-content
允许内容本身定义元素的宽度。如果有一个段落,该段落的宽度将是它里面的文本,没有任何换行。如果段落足够长,那么最终会出现一些水平滚动。
让我们重新看一下那个过于简单的例子,其中三个块级元素缩小并并排排列。这并非因为 flex-shrink
;而是因为当元素声明的宽度为 max-content
时,这就是它们的大小。这确实是它们的最大宽度,因为这与每个元素内组合内容的宽度一样。
这里,看一下这些元素,没有 Flexbox 执行 Flexbox 操作,但它们却有一个 width: max-content
所以,当只有一小段文本时,内在 max-content
会缩小元素,而不是 flex-shrink
。当然,Flexbox 也带来了它的默认 flex-direction: row
行为,它将 Flex 项变成列,并将它们并排放置。这里再看一下,但这次突出了空白区域。
flex-shrink
添加到方程式中
将 因此,我们看到声明 display: flex
会将该 max-content
内在大小应用于 Flex 项。这些项希望变得与它们的内容一样大。但是,这里还有另一个默认设置,那就是 flex-shrink
。
flex-shrink
基本上会查看灵活容器中的所有 Flex 项,以确保它们不会溢出父容器。如果 Flex 项能够在不溢出灵活容器的情况下并排排列,那么 flex-shrink
不会执行任何操作……它已经完成了自己的工作。
但如果 Flex 项确实溢出了容器(这要归功于 max-content
内在宽度设置),则允许 Flex 项缩小以防止溢出,因为 flex-shrink
会注意这种情况。
这就是 flex-shrink
的默认值为 1
的原因。如果没有它,事情会很快变得很混乱。
这就是列没有等宽的原因
回到我们的设计场景,我们想要在英雄区下面获得三个等宽的列,我们发现这些列没有等宽。这是因为 Flexbox 会先查看每个 Flex 项的内容大小,然后再考虑缩小它们。

为了简便起见,随着我们更深入地研究,让我们使用一些漂亮的整数。我们可以通过在 Flex 项上声明宽度来做到这一点。当我们在 Flex 项上声明宽度时,我们会将该内在大小抛诸脑后,因为我们现在已经声明了一个显式值。这使得弄清楚到底发生了什么变得容易得多。
在下面的 Pen 中,我们有一个宽度为 600px
的灵活容器(display: flex
)作为父容器。我已经删除了可能影响数字的所有内容,因此没有 gap
或 padding
。我还用 outline
替换了 border
,这样我们仍然可以轻松地可视化所有内容。
第一个和第三个 Flex 项的宽度为 300px
,中间一个是 600px
。如果我们将它们加起来,总计为 1200px
。这比父容器内的 600px
大,因此 flex-shrink
会发挥作用。
flex-shrink
是一个比率。如果所有元素都具有相同的 flex-shrink
(默认情况下为 1
),那么它们都会以相同的速率缩小。这并不意味着它们都会缩小到相同的大小或缩小相同的量,但它们都会以相同的速率缩小。
如果我们回到 Firefox DevTools,可以看到基本大小、flex-shrink
和最终大小。在这种情况下,两个 300px
元素现在是 150px
,而 600px
元素现在是 300px
。

300px
的元素变成了 150px
。
600px
的较大元素变成了 300px
。如果我们将所有三个 Flex 项的基本大小加起来(我们为它们声明的实际宽度),总计为 1200px
。我们的 Flex 容器的宽度为 600px
。如果我们将所有内容除以 2,它就适合了!它们都以相同的速率缩小,将它们自己的宽度除以 2。
在现实世界中,我们并不经常遇到这样的漂亮的整数,但我认为这很好地说明了 Flexbox 在确定元素大小时的工作原理。
使列等宽
有几种不同的方法可以使我们想要的三个列的宽度相等,但有些方法比其他方法更好。对于所有方法,基本思想是我们希望所有列的基本大小都相同。如果它们具有相同的基本大小,那么当 Flexbox 执行 Flex 操作时,它们会以相同的速率缩小(或增大,如果我们使用 flex-grow
),理论上,这应该使它们的大小相同。
有一些常见的实现方法,但我在深入研究这一切的过程中发现,我认为这些方法是有缺陷的。让我们看看两种我在实际应用中见到的最常见的解决方案,我将解释为什么它们不起作用。
flex: 1
方法 1:使用 我们可以尝试让所有 Flex 项具有相同基本大小的一种方法是在所有 Flex 项上声明 flex: 1
.flex-parent { display: flex; }
.flex-parent > * { flex: 1; }
在我以前制作的一个教程中,我使用了不同的方法,我一定有 100 个人问我为什么不使用 flex: 1
。我回答说我不想深入探讨 flex
简写属性。但后来我在一个新的演示中使用了 flex: 1
,令我惊讶的是,它不起作用。
这些列没有等宽。
这里的中间列比另外两列要宽。虽然宽度差距不大,但我创建的整个设计模式就是为了确保每次都能获得完全相等的列,无论内容如何。
那么为什么这种情况行不通呢?**罪魁祸首是中间列组件上的padding
**。
也许你会说,给其中一个项目添加填充而其他项目不添加填充很傻,或者我们可以嵌套元素(我们以后会讲到)。但在我看来,我应该能够拥有一个无论我们放置什么内容都能正常工作的布局。毕竟,如今的网页都是关于我们可以即插即用的组件。
当我第一次设置它的时候,我确信它会奏效,看到这个问题出现后,我迫切地想要了解这里到底发生了什么。
flex
简写不仅仅设置了flex-grow: 1
。如果你还不知道,flex
是flex-grow
、flex-shrink
和flex-basis
的简写。
这些组成属性的默认值是
.selector {
flex-grow: 0;
flex-shrink: 1;
flex-basis: auto;
}
我不会在这篇文章中深入探讨flex-basis
,因为Robin 已经很好地解释了它。现在,为了简单起见,我们可以把它看成width
,因为我们没有玩flex-direction
。
我们已经看到flex-shrink
是如何工作的。flex-grow
则相反。如果所有弹性项目沿主轴的大小都小于父元素,则它们可以增长以填满该空间。
所以默认情况下,弹性项目
- 不会增长;
- 如果它们会溢出父元素,则允许它们收缩;
- 它们的宽度就像
max-content
。
综上所述,flex
简写的默认值为
.selector {
flex: 0 1 auto;
}
flex
简写有趣的一点是,你可以省略值。因此,当我们声明flex: 1
时,它将第一个值flex-grow
设置为1
,这实际上打开了flex-grow
。
**这里奇怪的是,省略的值会发生什么**。你可能会认为它们会保持默认值,但实际上并非如此。不过,在我们了解发生了什么之前,让我们先深入了解一下flex-grow
究竟做了什么。
正如我们所见,flex-shrink
允许元素收缩,如果它们的基准大小加起来的值大于父容器的可用宽度。flex-grow
则相反。如果元素基准大小的总和小于父容器宽度值,则它们会增长以填满可用空间。
如果我们以一个非常基本的示例为例,我们在其中有三个并排的小div,并添加flex-grow: 1
,它们会增长以填满剩余空间。
但如果我们有三个宽度不相等的div——就像我们开始使用的那些div——向它们添加flex-grow
根本不会有任何作用。它们不会增长,因为它们已经占用所有可用空间了——事实上,空间太多了,以至于flex-shrink
需要介入并收缩它们以适应!
但是,正如人们向我指出的那样,设置flex: 1
可以用来创建相等的列。嗯,有点像我们上面看到的!在简单的场景中,它确实有效,正如你下面所看到的。
当我们声明flex: 1
时,它有效是因为,不仅它将flex-grow
设置为1
,而且它还改变了flex-basis
!
.selector {
flex: 1;
/* flex-grow: 1; */
/* flex-shrink: 1; */
/* flex-basis: 0%; Wait what? */
}
是的,设置flex: 1
将flex-basis
设置为0%
。这覆盖了我们之前拥有的像max-content
一样的内在大小。我们所有的弹性项目现在都想拥有0
的基准大小!

flex: 1
简写的扩展视图显示flex-basis
已更改为0
因此它们的基准大小现在是0
,但由于flex-grow
的存在,它们都可以增长以填满空闲空间。实际上,在这种情况下,flex-shrink
不再起任何作用,因为所有弹性项目的宽度现在都是0
,并且正在增长以填满可用空间。

flex: 1
的元素的内容大小为0
,并且正在增长以填满可用空间。就像之前的收缩示例一样,我们正在获取可用的空间,并让所有弹性项目以相同的速度增长。由于它们的基准宽度都是0
,因此以相同的速度增长意味着可用空间被它们平均分配,并且它们都具有相同的大小!
除了,正如我们所看到的,**并非总是如此……**
原因在于,当弹性盒子完成所有这些操作并分配空间时,无论是收缩还是增长弹性项目,它都在查看元素的内容大小。如果你还记得盒模型,我们有内容大小本身,然后是padding
、border
和margin
在它外面。
并没有,我没有忘记* { box-sizing: border-box; }
。
**这是 CSS 的一个奇怪之处**,但它确实有道理。如果浏览器查看了这些元素的宽度,并将它们的padding
和borders
包含在计算中,它如何才能收缩它们?要么padding
也必须收缩,要么东西会溢出空间。这两种情况都很糟糕,因此,在计算元素的基准大小时,它只查看内容框,而不是查看元素的box-size
!
因此,设置flex: 1
会导致在某些元素上有border
或padding
的情况下出现问题。我现在有三个内容大小为0
的元素,但中间元素有填充。如果我们没有这个填充,我们将拥有与我们查看flex-shrink
如何工作时相同的数学运算。
一个600px
的父元素和三个宽度为0px
的弹性项目。它们都有flex-grow: 1
,因此它们以相同的速度增长,并且最终每个宽度都为200px
。但填充把一切都搞砸了。相反,我最终得到了三个内容大小为0
的div,但中间元素具有padding: 1rem
。这意味着它的内容大小为0
,加上32px
的填充。
我们有600 - 32 = 568px
需要平均分配,而不是600px
。所有 div 都想以相同的速度增长,因此568 / 3 = 189.3333px
。
就是这样!
但是……请记住,那是它们的内容大小,而不是总宽度!这导致两个div的宽度为189.333px
,另一个的宽度为189.333px + 32 = 221.333px
。这是一个相当大的差异!
flex-basis: 100%
方法 2:我一直这样处理它
.flex-parent {
display: flex;
}
.flex-parent > * {
flex-basis: 100%;
}
我以为这方法很长时间了。实际上,这应该是我最终的解决方案,在展示了flex: 1
不起作用之后。但当我写这篇文章的时候,我意识到它也遇到了同样的问题,只不过没有那么明显。我用肉眼没有注意到它。
所有元素都试图成为100%
宽度,这意味着它们都想要匹配父元素的宽度,该宽度又是600px
(在正常情况下,它通常要大得多,这会进一步减少可感知的差异)。
问题是,100%
包含计算值中的填充(因为* { box-size: border-box; }
,为了这篇文章,我假设每个人都在使用它)。因此,外部div最终得到600px
的内容大小,而中间div最终得到600 - 32 = 568px
的内容大小。
当浏览器计算如何均匀分配空间时,它不是在查看如何将1800px
均匀地压缩到600px
空间中,而是在查看如何压缩1768px
。此外,正如我们之前看到的,弹性项目不会以相同的量收缩,而是以相同的速度收缩!因此,有填充的元素在总收缩量上比其他元素略少。
这会导致.card
最终宽度为214.483px
,而其他元素则为192.75px
。同样,这会导致宽度值不相等,尽管差异小于我们在flex: 1
解决方案中看到的差异。
为什么 CSS Grid 是更好的选择
虽然所有这些都让人有点沮丧(甚至有点令人困惑),但它们都是有原因的。如果在 flex 参与时,边距、填充或边框改变了大小,那将是一场噩梦。
也许这意味着 CSS Grid 可能是这种非常常见的设计模式的更好解决方案。
我一直认为 flexbox 比 grid 更容易上手,但 grid 从长远来看给了你更多最终的控制权,但它更难理解。不过,最近我改变了主意,我认为**grid 不仅在这种情况下给了我们更好的控制权,而且它实际上也更直观**。
通常,当我们使用 grid 时,我们会使用grid-template-columns
明确地声明我们的列。不过,我们不必这样做。我们可以使用grid-auto-flow: column
让它像 flexbox 一样运行。
.grid-container {
display: grid;
grid-auto-flow: column;
}
就这样,我们最终获得了与在其中添加display: flex
相同类型的行为。就像flex
一样,列可能会不平衡,但使用 grid 的优势在于,**父元素对所有内容都具有完全控制权**。因此,与其像在 flexbox 中那样让项目的内容产生影响,我们只需要再添加一行代码就可以解决问题
.grid-container {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
}
我喜欢这一点都在父选择器上,我们不需要选择子元素来帮助获得我们想要的布局!
这里有趣的是fr
单位是如何工作的。它们在规范中实际上被称为“弹性单位”,它们的工作方式与 flexbox 在划分空间方面一样。最大的区别在于:**它们会查看其他轨道以确定它们拥有多少空间**,而不会关注这些轨道内部的内容。
这才是真正的魔力。通过声明grid-auto-columns: 1fr
,我们实际上是在说,“默认情况下,我的所有列都应该具有相等的宽度”,这就是我们从一开始就想要的!
但是小屏幕呢?
我喜欢这种方法,因为它非常简单
.grid-container {
display: grid;
gap: 1em;
}
@media (min-width: 35em) {
grid-auto-flow: column;
grid-auto-columns: 1fr;
}
就这样,它完美地工作了。通过从一开始就声明 `display: grid`,我们可以包含 `gap` 来保持等间距,无论子元素是行还是列。
我还发现这比在媒体查询中更改 `flex-direction` 来获得类似结果更直观。尽管我很喜欢 flexbox(而且我认为它仍然有很好的用例),但声明 `flex-direction: column` 创建行,反之亦然,乍一看有点违反直觉。
当然,如果你喜欢不用媒体查询,没有什么能阻止你借助 `auto-fit` 将其提升到一个新的水平,这类似于 用 `flex-wrap` 设置一些东西(虽然不完全相同)
.grid-container {
display: grid;
gap: 1em;
grid-template-columns: repeat(auto-fit, minmax(10em, 25em));
}
用 flexbox 实现
我意识到,我们可以通过将包含 `padding` 的元素嵌套在 flexbox 中来解决这个问题。自从我们开始使用浮动制作布局以来,我们一直在这样做,Bootstrap 真正将这种设计模式推广开来。
<div class="container">
<div class="row">
<div class="col"> <div class="">... </div>
<div class="col"> <div class="element-with-padding">...</div> </div>
<div class="col"> ... </div>
</div>
</div>
这样做没有问题。它可以工作!但是浮动也起作用,近年来随着更好的解决方案的发布,我们已经停止使用它们进行布局。
我喜欢 grid 的原因之一是我们可以简化我们的标记。就像我们放弃了浮动以寻找更好的解决方案一样,至少我希望人们保持开放的心态,也许,也许,grid 可能是一个更好、更直观的解决方案,适用于这种设计模式。
当然,flexbox 仍然有它的位置
我仍然喜欢 flexbox,但我认为 **它真正的力量来自于我们想要依靠 flex 项目的内在宽度的时候**,例如导航,或者元素组,例如按钮或其他你想要并排放置的宽度不同的项目。
在这些情况下,这种行为是一个优势,它使不等项目之间的等间距变得轻而易举!flexbox 非常棒,我不会停止使用它。
但在其他情况下,当你发现自己在与 flexbox 的工作方式作斗争时,也许你可以转向 grid,即使它不是一个典型的“2D”网格,你通常不应该在那里使用它。
人们经常告诉我,他们很难理解 grid,因为它太复杂了,虽然它确实可以很复杂,但正如我们在这里看到的,它并不一定需要如此复杂。
很棒的文章。谢谢。
像你一样,我也使用 `grid-template-columns: repeat(auto-fit, minmax(10em, 25em));` 来解决这种问题。
是的!`auto-fit` 和 `minmax()` 太棒了,每次使用它都感觉像魔法一样!
你尝试过使用 `columns` CSS 属性(和其他相关属性)吗?
我认为 `columns` 是最不被使用的属性之一,对于某些事情来说,它非常棒!但由于它的工作原理,它在用例中有点边缘化……我想这就是它不被使用的原因,哈哈。
很棒的文章!但是我并不完全理解这一部分——“声明 `flex-direction: column` 创建行,反之亦然”。
如果你在父元素上声明 `flex-direction: row`,则子元素将成为列 :)
恕我直言,问题在于 Flexbox 和 Grid 都有略微不同的含义/思维模型。
Flexbox 说“我已经设置为列,所以我将成为一列”。
这很有道理,因为 _Flexbox 是一个“一维”布局_。
Grid 说“我已经设置为列,所以我将使所有子元素成为列”。
同样,这也非常有道理,因为 _Grid 是一个二维布局_。
感谢你写了这篇文章!非常感谢 :)
我在 iPhone 上阅读时发现,Grid 版本没有显示等宽列。
为了确保列具有相同的尺寸,你可以使用 `grid-auto-columns: minmax(0, 1fr)`。
哦,感谢你指出这一点!是的,1fr 在分配空间,但它不会低于 `min-width` 的尺寸,因此每列都会达到最长单词的宽度。让它达到 `0`,允许它们继续缩小……虽然我认为理想情况下,在这种情况下,我们根本没有列:D
关于 flexbox 的一些奇怪之处和强大功能的精彩文章。
我总是倾向于使用
为什么它不能在所有情况下都起作用?
它可以工作!它们没有缩小或增大,而是按照你给定的尺寸进行。不过,我不喜欢这种解决方案的原因是,代码更“脆弱”,因为它完全依赖于嵌套在其中的子元素数量。
对我来说,理想情况下,这更像是一种实用程序类,我可以使用它来获得等宽列,无论它会产生多少列,但同样,布局经常会发生变化,如果你在设计更新时从 3 列变为 4 列,则需要重新编写 CSS 以使其工作。这并不是世界末日,应该很容易修复,但如果有一些方法可以做到同样的事情,而不用更新 CSS(或更改 HTML 中使用的类),那在我看来会更好:D
@Kevin Powell 为了反驳。设计显示 3 列,这将是 33.33%,所以在我看来代码不会脆弱。它将是 33.33%,除了在移动设备上。就像 4 将是 25% 等等。
你能解释一下“脆弱”吗?
在子元素上使用 `flex: 1 0 33%` 就完成了?我是不是漏掉了什么?
我不喜欢使用固定的百分比,因为如果我更新了一些东西,它变成了 4 个子元素,或者 2 个子元素,我需要更新的不仅仅是添加一个新组件并使其工作……使用 grid 方法,我可以将其重新用于任何地方,无论子元素数量或设置方式如何。一个可以工作的类,而不是一堆类,使生活变得更加轻松 :)
这是一个有点魔法的数字,但 `flex-basis: 33.33%` 可以完成任务。
正如你所说,它有点像魔法数字。我应该也使用 2 列或 4 列的一些示例。
我喜欢有一个可以处理任何情况的类(如果可能的话),而不是一堆类,在需要时选择正确的类,并在内容更新时更改它 :)
在 Webkit 中,我发现有时在 flex 父元素上添加边框会创建一个间隙,无论如何子元素都不会填充这个间隙。讨厌的 Webkit……切换到 grid 就解决了。
嗨,Kevin,感谢你写了这篇很棒的文章。假设我有 4 列。每列都有不同的单词,它们的长度不相等。第一个单词是“a”,第二个单词是“bb”,第三个单词是“ccc”,最后一个单词是“zzzzzzzzzzzzzzzzz”。我想让所有列都具有相同的尺寸。我说是 4 列,但它可能会改变。我该如何解决这个问题?
那些建议使用 33.333% 的人没有抓住重点。当作者提到脆弱性时,他的意思只是他希望事物能够动态地工作,因为我们经常需要为特性 x 添加另一列,或者因为某个特性不再相关而删除一列。
我认为设置 flex-item 固定宽度的最佳方式是为这些项目设置最小宽度(min-width: 50%、25% ……等等),这将覆盖 flex-grow 或 flex-shrink 的效果。
但是如果我想使用 flex-basis 使我的列在没有 @media 查询的情况下换行呢?
当列容器大于 60rem 时,它们是相等的。当容器宽度介于 50 和 60 rem 之间时,列 2 会缩小。当容器小于 50rem 时,它们会换行。
我通过设置以下内容解决了问题:
但这看起来更像是一种技巧。有没有什么“正确”的解决方案来解决这种问题?
很棒的解释!在创建我的设计时,要考虑很多因素。我完全同意 Flex 和 Grid 服务于不同的目标,并且具有不同的思维模式。当我想到 Grid 时,我会想到网格模板,这让我更着迷。Flex 是我最喜欢的工具,用于在所有地方对齐项目、行和列,而不是用于创建重复模式。感谢您让我阅读您的文章,凯文!
flex-basis: 30%;
将其设置在子元素上,即使使用间隙也能正常工作。
似乎在 flexbox 和 grid-auto-flow: column 中,布局的高度(在网格的情况下是行数)决定了下一列的开始位置。
我正在寻找一种方法,使布局的宽度定义最大列数,并使列的断点能够最大程度地利用整个宽度,从而使最终高度尽可能低。