Flexbox交互式指南

本文翻译自《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是Fl​​exbox布局中一个相对较新的功能,但自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语言建立一个正确的直觉时,事情开始发生变化,它将成为一种绝对的乐趣。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注