上一篇最后留了一句话:inline-block 的三个坑都来自“行内上下文是为排文字设计的,不是为排一排方块设计的”,所以 Flexbox 接手了。这一篇就来回答接手之后的关键问题:给父元素加上 display: flex,到底哪些规则变了;以及随之而来的主轴、交叉轴怎么判断,justify-content 和 align-items 分别在对齐哪个方向。

它改的是父元素的“内层类型”,以及孩子的身份

上一篇讲过,display 一直在同时描述两件事:外层类型决定这个盒子怎么和邻居排,内层类型决定它内部的孩子怎么排。display: flex 改的是 后者。

按规范的定义,flex 等同于 block flex:外层依旧是 block,所以这个父元素仍然独占一行、上下接着别的块级元素,行为和普通 div 没有区别;变的是它内部——从常规流(normal flow)换成了 flex 布局 14。写成 inline-flex 就是 inline flex:外层变成行内级,会跟着文字首尾相接,内部同样是 flex 布局 14。日常做整行布局,用 flex 就够了。

这个父元素在术语里叫 flex container(弹性容器),它内部参与 flex 布局的那些孩子叫 flex item(弹性项目)。这里有三个马上会遇到的后果:

第一,只有直接子元素变成 flex item。 孙子辈及更深的后代不受影响,仍然按原来的规则排 3。想让某一层里的东西并排,display: flex 必须写在它们共同的那一层父元素上。

第二,子元素的 display 会被“块化”:只要最近的一层祖先算出来是 flex 或 inline-flex,这个子元素自己算出来的 display 就被转换成块级版本 3。所以上一篇那个难题——“span 不能设宽高”——在 flex 容器里自动消失:作为 flex item 的 span 可以设 width、height 和上下 margin,行内身份被换成了块级身份。(只有成为 flex item 的那一层享受这个待遇。)

第三,一批为块级常规流设计的规则在这里失效。 flex 容器建立的是一个 flex formatting context:13

  • 子元素上的 float、clear 不再产生浮动和清除,也不会让元素脱离常规流;
  • vertical-align 对 flex item 没有效果,对齐改由 flex 的属性负责;
  • 相邻 flex item 的外边距不再合并。上一篇里“上下两个块级盒子间距取较大者”的规则,在 flex 容器内部不成立;
  • 容器自己的外边距也不会和内容的外边距合并。

一句话概括:display: flex 写在父元素上,改的却是孩子——孩子的排法变了,孩子的身份被改成块级,一批旧的块级规则被换掉了。

主轴由 flex-direction 决定,交叉轴就是剩下那条

flex 布局里所有对齐属性都建立在两条轴上 2:

  • 主轴(main axis):由 flex-direction 指定的那条方向,它的默认值是 row;
  • 交叉轴(cross axis):与主轴垂直的那条。

默认的 flex-direction: row 表示主轴沿着文字行走的方向——在中文、英文这种从左到右的文字里,就是水平向右。所以默认情况下主轴水平、交叉轴竖直。改成 flex-direction: column,主轴变成竖直向下,交叉轴就变成水平方向 2。

判断顺序只有一步:先看 flex-direction,确定主轴;剩下那条就是交叉轴。 不要按“横的是主轴、竖的是交叉轴”去记,那个印象在做侧边栏、竖向工具栏时立刻会出错。

方向上还有一个细节:flex-start / flex-end 指的是 主轴的 起点和终点,会跟着 flex-direction 走,换了方向它们指的位置也跟着换 2。

在尺寸上,flex item 有两条默认行为,先记住,下面两节都要用到:2

  • 主轴方向默认不长大:三个 100px 的卡片并排时,它们就保持各自 100px,不会自动撑开去填满容器——这正是下面 justify-content 能分到“富余空间”的前提。但空间不够时会 被压缩,而不是溢出容器。
  • 交叉轴方向默认被拉伸:这一条来自 align-items 的初始值 stretch,后面会专门讲。

justify- 管主轴,align- 管交叉轴

flexbox 的对齐属性只有一条记忆规则:justify- 开头的管主轴,align- 开头的管交叉轴 5。它跟哪条轴是横、哪条是竖没有关系,所以换 flex-direction 时属性名不用改,管的方向自动跟着换。

flex-direction主轴justify-content 对齐的方向交叉轴align-items 对齐的方向
row(默认)水平左右竖直上下
column竖直上下水平左右

顺带一提:justify-items 和 justify-self 在 flex 布局里会被忽略,写了没有效果——主轴方向上,flex 永远把这一行元素当作一个整体来移动 2。

justify-content:分配的是主轴上多出来的那部分空间

justify-content 的初始值是 normal,行为等同于 flex-start 25。它做的事可以说得很具体:先算出主轴上的 富余空间——容器的可用主轴尺寸减去所有子项排完后占用的主轴尺寸——再决定把这些空间放到哪里 2。

  • flex-start:空间全放到后面,子项挤在主轴起点;
  • center:空间分成两半,子项整体居中;
  • flex-end:空间全放到前面;
  • space-between:把空间平均分到每两个相邻子项之间,两端不留;
  • space-around:每个子项两侧各留一份,于是相邻间距是两端间距的两倍;
  • space-evenly:每个间隙(包括两端)完全等宽。

这里有一个前提值得单独点出来:主轴上一旦没有富余空间,这几个值看起来全都一样。 这解释了一个很常见的疑问:“flex-direction: column 时 justify-content 好像没反应。”原因是容器的高度由内容撑开,内容刚好占满,自然没有多余空间可分配。想看到效果,得给容器一个明确的高度(比如 height: 100vh 或固定像素)。而在默认的 row 下,块级容器默认占满父级宽度,子项又不会自动长大,通常本来就有富余宽度,所以效果一眼可见。

align-items:默认 stretch 才是“卡片自动等高”的来源

align-items 的初始值是 stretch 2。这就是很多人第一次用 flex 时觉得意外的那件事:随便写三张内容长度不同的卡片,它们会自动一样高。这不是巧合,而是每个子项在交叉轴方向被拉伸到了整行的高度。

改成 flex-start、center、flex-end 之后,子项不再被拉伸,各自回到自己的内容高度,然后在这一行内靠上、居中或靠下。

交叉轴的“富余空间”来自哪里,和主轴不太一样:它主要来自 这一行里最高的那个元素,或者你给容器设的交叉轴尺寸。所以 align-items: center 通常不需要给容器写 height 也能看到居中效果——矮的卡片会在“最高那张撑出来的行高”里居中。而 justify-content 在 column 下则必须先有容器高度,这个不对称值得记住。

一个最小可用的例子

1<div class="row">
2  <div class="card">A</div>
3  <div class="card">B</div>
4  <div class="card">C</div>
5</div>
1.row {
2  display: flex;
3  height: 120px;
4  justify-content: space-between; /* 主轴(水平):富余宽度分到卡片之间 */
5  align-items: center;            /* 交叉轴(竖直):每张卡片在行高里居中 */
6  background: #f1f5f9;
7}
8
9.card {
10  width: 100px;
11  padding: 8px 12px;
12  background: #dbeafe;
13}

三张卡片并排、左右两端贴住容器、竖直方向各自居中。把 align-items 那一行删掉,卡片立刻被拉伸成 120px 高——这就是 stretch 默认值在做的事。把 flex-direction 改成 column,justify-content 会立刻改成分配竖直方向的富余空间,而 align-items 变成管左右对齐。

到这里,flex 布局最核心的一步就走完了:写在父元素上的 display: flex 改变的是孩子的排法和身份;主轴由 flex-direction 决定,交叉轴是另外那条;justify-content 分配主轴的富余空间,align-items 决定子项在交叉轴上怎么站,而它的默认值 stretch 正是卡片自动等高的原因。 子项多了怎么换行、谁该长大谁该缩小,是后面几篇的内容。