本文翻译自《An Interactive Guide to Flexbox》。
Flexbox(弹性框)是一种非常强大的布局模式。当我们真正理解它的工作原理时,我们可以构建自动响应的动态布局,并根据需要重新排列。
例如,看看这个:
(译者注:案例请看原文)
此案例很大程度上受到Adam Argyle的出色的“4种布局,1种价格”的启发。它不使用媒体/容器查询。它没有设置任意断点,而是使用流畅的原则来创建无缝流动的布局。 以下是此案例的CSS:
form {
display: flex;
align-items: flex-end;
flex-wrap: wrap;
gap: 16px;
}
.name {
flex-grow: 1;
flex-basis: 160px;
}
.email {
flex-grow: 3;
flex-basis: 200px;
}
button {
flex-grow: 1;
flex-basis: 80px;
}
我记得我曾经遇到过这样的演示,当时完全不知所措。我知道 Flexbox 的基础知识,但这似乎是绝对的魔法!
在这篇博文中,我想完善您对 Flexbox 的心理模型。我们将通过了解每个属性来建立对 Flexbox 算法如何工作的直觉。无论您是 CSS 初学者,还是已经使用 Flexbox 多年,我敢打赌您都会学到很多东西!
让我们开始吧!
内容警告
我在本教程后面会做一个与食物相关的比喻。
Flexbox简介
CSS由许多不同的布局算法组成,布局算法的正式名称是“布局模式(layout mode)”。每种布局模式都是CSS中的一种小语言。默认布局模式是流式布局(Flow layout),但我们可以通过更改父容器上的display属性来选择使用Flexbox布局模式:
(译者注:案例请看原文)
当我们将display属性的值设置为flex时,我们会创建一个“flex格式化上下文”。这意味着,所有子元素都将根据Flexbox布局算法进行定位。
每种布局算法都旨在解决特定问题。默认的“流式”布局旨在创建数字文档,它本质上是Microsoft Word的布局算法。标题和段落以块的形式垂直堆叠,而文本、链接和图像等内容则不显眼地位于这些块内。
那么,Flexbox解决了什么问题?Flexbox就是将一组条目(元素)排列成一行或一列,并赋予我们对这些项目的分布和对齐方式的极大控制权。顾名思义,Flexbox就是“灵活性”。我们可以控制条目是增大还是缩小,如何分配额外的空间等等。
它仍然有意义吗?
你可能想知道:既然CSS Grid在现代浏览器中得到了很好的支持,那么Flexbox不是过时了吗?
CSS Grid是一种很棒的布局模式,但它解决的问题与Flexbox不同。我们应该学习这两种布局模式,并使用正确的工具来完成工作。
当涉及到以垂直或水平列表排列项目的动态、流畅的UI时,Flexbox仍然占据主导地位。我们将在本指南中看到一个示例,即解构的煎饼,这无法通过CSS Grid轻松实现。
老实说,作为一个熟悉CSS Grid和Flexbox的人,我仍然发现自己经常使用Flexbox!
Flex方向
如上所述,Flexbox主要是控制行或列中元素的分布。默认情况下,条目将并排堆叠在一行中,但我们可以使用flex-direction属性翻转到一列:
(译者注:案例请看原文)
使用flex-direction: row时,主轴水平运行,从左到右。当我们翻转为flex-direction: column时,主轴垂直运行,从上到下。
在Flexbox中,一切都基于主轴。算法不关心垂直或水平,甚至行或列。所有规则都围绕此主轴和垂直延伸的横轴构建。
这非常酷。当我们学习Flexbox的规则时,我们可以无缝地从水平布局切换到垂直布局。所有规则都会自动适应。此功能是Flexbox布局模式所独有的。
默认情况下,子元素将根据以下2条规则进行定位:
- 主轴(primary axis):子元素将聚集在容器的起始处。
- 交叉轴(cross axis):子元素将伸展以填满整个容器。
以下是这些规则的快速可视化:
在Flexbox中,我们决定主轴是水平还是垂直。这是所有Flexbox计算的根源。
“主轴”和“交叉轴”也可以称它们为“主轴”和“副轴”。
对齐
我们可以使用justify-content属性来改变子元素沿主轴的分布方式:
(译者注:案例请看原文)
当谈到主轴时,我们通常不会考虑对齐单个子项元素。相反,我们关心的是整个(子项元素)组的分布。
我们可以将所有项目集中在一个特定位置(使用flex-start、center和flex-end),也可以将它们分开(使用space-between、space-around和space-evenly)。
对于副轴,情况略有不同。我们使用align-items属性:
(译者注:案例请看原文)
有趣的是…使用align-items,我们有一些与justify-content相同的选项,但并没有完美的重叠。
为什么它们不共享相同的选项?我们很快就会解开这个谜团,但首先,我需要分享另一个对齐属性:align-self。
与justify-content和align-items不同,align-self应用于子元素,而不是容器。它允许我们沿副轴更改特定子元素的对齐方式:
(译者注:案例请看原文)
align-self具有与align-items相同的所有值。事实上,它们改变的是完全相同的东西。align-items是一种语法糖,是一种方便的简写,可以自动同时设置所有子元素的在副轴上的对齐方式。
没有justify-self属性。要理解为什么没有,我们需要深入研究Flexbox算法。
内容 vs 条目(子元素)
因此,根据我们目前所学的知识,Flexbox可能看起来相当随意。为什么名字是justify-content和align-items,而不是justify-items或align-content?
还有上文那个问题,为什么有align-items属性,而没有justify-self属性?
这些问题涉及到Flexbox最重要的和最容易被误解的事情之一。为了帮助解释,我想用一个比喻来说明。
在Flexbox中,条目(子元素)沿主轴分布。默认情况下,它们并排整齐排列。我可以画一条水平直线,将所有子元素串起来,就像烤肉串一样:
但是,交叉轴(副轴)有所不同。垂直直线只会与其中一个子项相交。它不像烤肉串,而更像一组鸡尾酒香肠:
这里有一个显著的区别。对于鸡尾酒香肠,每条都可以沿着签子移动,而不会干扰任何其他香肠:
相比之下,由于我们的主轴将每个子元素串在一起,因此单个元素无法沿着轴移动,而不会碰到其兄弟元素!尝试将中间部分左右拖动:
这是主轴和副轴之间的根本区别。当我们谈论副轴上的对齐时,每个条目都可以做任何它想做的事情。然而,在主轴上,我们只能考虑如何整组分配。
这就是为什么没有justify-self属性。中间部分设置justify-self: flex-start意味着什么?那个位置已经有另一个条目了!
考虑到所有这些知识背景,让我们对讨论的所有4个术语进行适当的定义:
- justify — 沿主轴定位某物。
- align — 沿交叉轴(副轴)定位某物。
- content — 一组可以排列的“东西”。
- items — 可以单独设置位置的单个条目。
因此:我们有justify-content属性来控制组沿主轴的排列,我们有align-items属性来沿副轴单独定位每个条目。这是我们用来管理Flexbox布局的两个主要属性。
没有justify-items属性的原因与没有justify-self属性的原因相同;当谈到主轴时,我们必须将所有条目(子元素)视为一个组。
那么align-content属性呢?实际上,它确实存在于Flexbox中!稍后我们将在讨论flex-wrap属性时介绍它。
假尺寸(hypothetical size)
让我们来谈谈我对Flexbox最令人大开眼界的认识之一。假设我有以下 CSS:
.item {
width: 2000px;
}
一个理性的人看到这个可能会说:“好吧,所以我们会得到一个宽度为2000像素的项目”。但这总是正确的吗?让我们测试一下:
(译者注:案例请看原文)
这很有趣,不是吗?两个条目应用了完全相同的CSS。它们各自的宽度都是:2000px。然而,第一个条目比第二个条目宽得多!
不同之处在于布局模式。第一个条目使用流式布局进行渲染,在流式布局中,宽度是一个硬性约束。当我们设置宽度:2000px时,我们将得到一个2000像素宽的元素,即使它必须冲破视口的一侧。
然而,在Flexbox中,width属性的实现方式不同。它更像是一种建议,而不是硬性约束。规范对此有一个名称:假设尺寸。它是元素在完美的乌托邦世界中的尺寸,当没有任何阻碍时。
唉,事情很少这么简单。在这种情况下,限制因素是父元素没有空间容纳2000px宽的子元素。因此,子元素的尺寸会缩小以适应它。
这是Flexbox理念的核心部分。事物是流动的、弹性的,可以适应世界的限制。
算法的输入
我们倾向于将CSS语言视为属性的集合,但我认为这是错误的思维模型(mental model)。正如我们所见,width属性的行为会根据所使用的布局模式而有所不同!
相反,我喜欢将CSS视为布局模式的集合。每种布局模式都是一个可以实现或重新定义每个CSS属性的算法。我们通过CSS声明(键/值对)提供一个算法,然后该算法决定如何使用它们。
换句话说,我们编写的CSS是这些算法的输入,就像传递给函数的参数一样。如果我们想真正熟悉CSS,仅仅学习CSS属性是不够的;我们必须了解算法如何使用这些属性。(译者注:这就是很多CSS新手说“CSS很难学”、“CSS属性的功能不正交”的原因了,因为学习和使用CSS的思维模式不正确)
增长与收缩
因此,我们已经看到Flexbox算法具有一些内建的弹性,具有假尺寸。但要真正了解Flexbox的弹性,我们需要讨论3个属性:flex-grow、flex-shrink和flex-basis。 让我们看看这些属性。
flex-basis
我承认:很长一段时间以来,我都不太明白flex-basis到底是什么。
简单来说:在Flex行中,flex-basis的作用与width相同。在Flex列中,flex-basis的作用与height相同。
我们已经知道,Flexbox中的所有内容都与主轴和副轴挂钩。例如,justify-content将沿主轴分布子元素,无论主轴是水平还是垂直,其工作方式都完全相同。
但是 width和height不遵循这条规则!width总是会影响水平尺寸。当我们将flex-direction从row翻转为column时,它不会突然变成height。
因此,Flexbox的作者创建了一个通用的“尺寸”属性,称为flex-basis。它类似于width或height,但与其他所有东西一样与主轴挂钩。它允许我们在主轴方向上设置元素的假尺寸,无论主轴方向是水平还是垂直。
在这里尝试一下。每个子元素都被赋予了flex-basis: 50px,但你可以调整第一个子元素:
(译者注:案例请看原文)
就像我们在width方面看到的一样,flex-basis更多的是一种建议尺寸,而不是硬性约束。在某个时候,空间不足以让所有元素都按照指定的大小放置,因此它们必须做出妥协,以避免溢出。
不完全一样
通常,我们可以在Flex行(Flex row)中互换使用width和flex-basis,但也有一些例外。例如,width属性对图像等替换元素的影响与flex-basis不同。此外,width可以将元素缩小到其最小尺寸以下,而flex-basis则不能。
这远远超出了这篇博文的范围,但我想提一下,因为你可能偶尔会遇到两个属性具有不同效果的边界情况。
flex-grow
默认情况下,Flex上下文中的元素将沿主轴缩小到其最小舒适尺寸。这通常会产生容器元素的额外的空间。
我们可以使用flex-grow属性来指定如何使用该空间:
(译者注:案例请看原文)
flex-grow的默认值是0,这意味着flex-grow是可选的。如果我们想让某个子元素占用容器中的所有额外空间,我们需要明确地告诉它。
如果多个子元素设置了flex-grow会怎么样?在这种情况下,额外空间会根据子元素的flex-grow的值按比例分配给它们。
我认为用可视化方式解释会更容易。尝试增加/减少每个子元素的flex-grow的值:
(译者注:案例请看原文)
例如,第一个子元素想要4个单位的额外空间,而第二个子元素想要1个单位。这意味着单位总数是5 (=4 + 1)。每个子元素都会按比例获得该额外空间的份额(第一个子元素获得4/5的额外空间,第二个子元素获得1/5的额外空间)。
flex-shrink
在目前我们看到的大多数示例中,我们都有额外的空间可供使用。但如果我们的子元素对于容器来说太大了怎么办?
让我们测试一下。尝试缩小容器的宽度以查看会发生什么:
(译者注:案例请看原文)
很有趣,对吧?两个子元素都会缩小,但它们会按比例缩小。第一个子元素的宽度始终是第二个子元素的2倍。
温馨提醒,flex-basis的作用与width相同。我们将使用flex-basis,因为它是常规用法,但如果使用width,我们也会得到完全相同的结果!
flex-basis和width设定元素的假设尺寸。Flexbox算法可能会将元素缩小到低于它所需尺寸,但默认情况下,它们将始终一起缩放,从而保持两个元素之间的比例。
现在,如果我们不想让元素按比例缩小怎么办?这就是flex-shrink属性的作用所在。
花几分钟看看这个演示。看看你是否能弄清楚这里发生了什么。我们将在下面进行探索。
(译者注:案例请看原文)
我们有两个子元素,每个元素的假设尺寸为250px。容器的宽度至少需要500px,才能容纳假设尺寸的这些子元素。
假设我们将容器缩小到400px。好吧,我们无法将价值500px的内容塞进400px的袋子里!我们缺口是100px。我们的元素将需要放弃总共100px,才能适合。
flex-shrink属性让我们决定如何支付这笔差额。
和flex-grow一样,它是一个比例。默认情况下,两个子元素都有flex-shrink: 1,因此每个子元素都支付缺口的1/2。它们各自放弃50px,其实际尺寸从250px缩小到200px。
现在,假设我们将第一个子元素的flex-shrink设置为3:
我们总共缺口100px。通常情况下,每个子元素都会支付1/2,但是由于我们对flex-shrink进行了调整,第一个元素最终支付3/4(75px),第二个元素支付1/4(25px)。
请注意,绝对值并不重要,重要的是比率。如果两个子元素都具有flex-shrink: 1,则每个子元素将支付总缺口的1/2。如果两个子元素都设置为flex-shrink: 1000,则每个子元素将支付总缺口的1000/2000。无论哪种方式,结果都是一样的。
收缩和比例
在我们查看的示例中,两个Flex子元素具有相同的假设尺寸(250px)。在确定如何收缩它们时,我们可以使用flex-shrink进行专门计算。
不过,正如我们之前所看到的,收缩算法也会尝试保持兄弟元素之间的比例。如果第一个子元素的大小是第二个的2倍,那么第一个子元素的收缩幅度会更大。
因此,完整的计算涉及每个子元素的flex-shrink值和相对大小。
不久前,我对flex-shrink有了新的认识:我们可以将其视为flex-grow的“反面”。它们是同一枚硬币的两面:
- flex-grow控制当子元素小于其容器时如何分配额外空间。
- flex-shrink控制当子元素大于其容器时如何移除空间。
这意味着同一时刻,这两个属性中只有一个可以处于激活的状态。如果有多余的空间,flex-shrink不会产生任何效果,因为子元素不需要缩小。如果子元素对于其容器来说太大,flex-grow不会产生任何效果,因为没有多余的空间可以分配。
我喜欢把它想象成两个不同的世界。你要么在地球上,要么在相反的世界。每个世界都有自己的规则。
防止收缩
有时,我们不希望某些Flex子元素收缩。
我经常在SVG图标和形状中发现这种情况。让我们看一个简化的示例:
(译者注:案例请看原文)
当容器变窄时,我们的两个圆圈会被挤压成椭圆形。如果我们想让它们保持圆形怎么办?
我们可以通过设置flex-shrink: 0来实现这一点:
(译者注:案例请看原文)
当我们将flex-shrink设置为0时,基本上完全“选择退出”收缩过程。Flexbox算法会将flex-basis(或width或height)视为硬性最小限制。
如果你感兴趣,以下是此演示的完整代码:
(译者注:代码请看原文)
更简单的方法?
有时,有人会想知道为什么我们有更简单的方法可以使用,却要费尽心思使用flex-shrink:
.item.ball {
min-width: 32px;
}
几年前,我会同意这一点。如果我们设置最小宽度,子元素将无法缩小到该点以下!我们添加了一个硬约束,而不是width / flex-basis的软约束。
我认为这是很容易将“熟悉”与“简单”混淆的情形之一。你可能对min-width比flex-shrink更熟悉,但这并不意味着flex-shrink更复杂!
经过几年的实践,我实际上觉得设置flex-shrink: 0是解决这个特定问题的更直接的方法。不过,min-width在Flexbox算法中仍然发挥着重要作用!我们接下来会讨论这一点。
最小尺寸陷阱
这里还有一件事需要讨论,而且非常重要。这可能是整篇文章中最有帮助的事情!
假设我们正在为一家电子商务商店网页构建一个流畅的搜索表单:
(译者注:案例请看原文)
当容器收缩到某个点以下时,内容就会溢出!
但为什么呢?flex-shrink的默认值是1,而我们并没有把它设置为0(阻止收缩),所以搜索输入框应该能够根据需要收缩!为什么它拒绝收缩呢?
情况是这样的:除了假设尺寸之外,Flexbox算法还关心另一个重要尺寸:最小尺寸(the minimum size)。
Flexbox算法拒绝将子元素缩小到其最小尺寸以下。无论我们将flex-shrink调到多高,内容都会溢出,而不是进一步缩小!
文本输入框的默认最小尺寸为170px-200px(不同浏览器会有所不同)。这就是我们上面遇到的限制。
在其他情况下,限制因素可能是元素的内容。例如,尝试调整此容器的大小:
(译者注:案例请看原文)
对于包含文本的元素,最小宽度是最长的不可断开的单词字符串的长度。 好消息是:我们可以使用min-width属性重新定义最小尺寸。
(译者注:案例请看原文)
通过直接在Flex子元素上设置min-width: 0px(译者注:在Flex容器的直接子元素上设置min-width: 0px,如果在孙元素上设置min-width: 0px是没有效果的),我们告诉Flexbox算法覆盖内置(默认)最小宽度。因为我们将其设置为0px,所以元素可以根据需要缩小。
相同的技巧可以在具有min-height属性的Flex列中发挥作用(尽管这个问题似乎并不经常出现)。
注意事项
值得注意的是,内置的最小尺寸确实有其用途。它起到护栏的作用,防止发生更糟糕的事情。
例如:当我们将min-width: 0px应用于包含文本的Flex子元素时,情况会变得更糟:
(译者注:案例请看原文)
能力越大,责任越大,对于Flexbox来说,min-width 一个特别强大的属性。它不止一次让我摆脱困境,但我总是小心翼翼,确保不会让事情变得更糟!
间隙
近年来提升Flexbox开发体验的最大的改进之一是gap属性:
(译者注:案例请看原文)
gap允许我们在每个Flex子元素之间创建间隙。这对于水平导航栏的标题子元素等场景非常有用:
(译者注:案例请看原文)
gap是Flexbox布局中一个相对较新的功能,但自2021年初以来,它已在所有现代浏览器中实现。
自动边距(Auto margins)
我还想分享一个与间距相关的技巧。这个技巧早在Flexbox早期就出现了,但相对来说不太为人所知,当我第一次发现它时,我感到非常震惊。
margin属性用于在特定元素周围添加空间。在某些布局模式(如Flow和Positioned)中,它甚至可以使用margin: auto将元素居中。
Flexbox中的自动边距更加有趣:
(译者注:案例请看原文)
之前,我们看到了flex-grow属性如何吞噬任何额外的空间,并将其应用于子元素。
自动边距将占用额外空间,并将其应用于元素的边距(margin)。它使我们能够精确控制在何处分配额外空间。
常见的顶部水平导航栏的布局是一侧显示logo,另一侧显示一些导航链接。以下是使用自动边距构建此布局的方法:
(译者注:案例请看原文)
Corpatech logo是列表中的第一个列表项。通过为其指定margin-right: auto,我们收集了所有额外空间,并将其强制置于第1个和第2个列表项之间。
我们可以使用浏览器devtools查看这里发生的情况:
有很多其他方法可以解决这个问题:我们可以将导航链接分组到它们自己的Flex容器中,或者我们可以使用flex-grow来扩展第一个列表项的宽度。但就我个人而言,我喜欢自动边距这个解决方案。我们将额外的空间视为资源,并决定它应该放在哪里。
换行(Wrapping)
到目前为止,我们已经讲了很多内容。我还想分享一个重要的收获。
到目前为止,我们所有的项目都并排放在一行/一列中。flex-wrap属性允许我们改变这种情况。看看吧:
(译者注:案例请看原文)
大多数时候,当我们在二维空间中工作时,我们会想要使用CSS Grid布局,但Flexbox + flex-wrap绝对有其用途!这个特殊的例子展示了“解构煎饼(deconstructed pancake)”布局,其中3个子元素在中等尺寸的屏幕上堆叠成倒金字塔形状。
当我们设置flex-wrap: wrap时,子元素不会缩小到其假设尺寸以下。至少,当可以选择换行到下一行或列时就不会缩小!
但是等一下!flex-wrap: wrap对我们上文的烤肉串/鸡尾酒香肠的比喻有什么影响?
使用flex-wrap: wrap,我们不再需要一条主轴线来串起每个子条目。实际上,每行都充当自己的迷你弹性盒容器。每行都有自己的一串,而不是所有行共用一串:
在这个缩小的范围内,我们到目前为止学到的所有规则仍然适用。例如,justify-content会在每根签子上分布两个子块。
但是align-items属性又怎么工作呢,现在我们有了多行的情况下?副轴现在可以与多个子条目相交了!
花点时间考虑一下。你认为当我们改变这个属性时会发生什么?一旦你有了答案(或至少有一个想法),看看它是否正确:
(译者注:案例请看原文)
每一行都是它自己的迷你Flexbox环境。align-items将在环绕每一行的隐形框内将每个子条目向上或向下移动。
但是如果我们想让整行(包括所有换行)本身对齐呢?我们可以使用align-content属性来实现: (译者注:案例请看原文)
总结一下以上案例发生的事情:
- flex-wrap: wrap给我们两行条目
- 在每一行中,align-items可以让我们向上或向下滑动每个单独的子条目
- 然而这两行在一个Flex上下文中!交叉轴(副轴)现在将与这两行相交,而不是一行。因此,我们不能单独移动其中一行,我们需要将它们作为一个组进行布局。
- 使用上面的定义,我们处理的是内容,而不是条目。但我们仍然在谈论副轴!因此,我们想要的属性是align-content。
你做到了!
所以我想承认一件事:这是一个很长的教程。我们已经陷入了困境,除非你已经是Flexbox的专业人士,否则我预计你的头会有点晕。
与CSS中的许多内容一样,Flexbox在你刚开始使用时可能看起来很简单,但当你学完基础知识时,复杂性会迅速增加。
因此,我们中的许多人在CSS方面都达到了早期的平稳期。我们有足够的知识来完成事情,但这是一场持续的斗争。CSS语言给人一种摇摇欲坠、难以捉摸的感觉,就像一座古老的绳桥,随时都可能断裂。当它断裂时,我们会向问题抛出随机的StackOverflow代码片段,希望能有所帮助。
这不好玩。这很糟糕,因为CSS是大多数前端开发工作中相当大的一部分! 事实上,CSS是一种非常健壮和一致的语言。问题是,我们的大多数心理模型都是不完整和不准确的。当我们花时间为CSS语言建立一个正确的直觉时,事情开始发生变化,它将成为一种绝对的乐趣。