上一篇解决了一个盒子自己有多大:width 量的是内容区还是边框外沿,padding 和 border 是加上去还是吃进去。但页面上的盒子不止一个,接下来要问的是它们 彼此之间怎么摆。给三个 div 各写 width: 200px,它们不会并排,而是一个叠一个;把 div 换成 span,却又无法给 span 设宽高。
这两件事是同一套规则的两面:默认排版规则(normal flow,常规流)先看每个盒子的 外层类型,决定它和邻居怎么排;再看 内层类型,决定它自己的孩子怎么排 1。理解了这两个关键词,div 和 span 的差别、以及 inline-block 为什么存在,就都能自己推出来。
块级盒子:从新的一行开始,一个接一个往下排
在块级格式化上下文里,盒子从包含块顶部开始,一个接一个 垂直 排列 1。<div>、<p>、<h1>、<ul> 这些元素的默认 display 就是 block。
这里有两个容易混淆的点,值得分开说。
第一,块级盒子一定 另起一行,后面的块级元素也一定再起一行。width 设成 200px 时,盒子本身就占 200px 宽,右侧确实空着,但那个空间 不会被下一个块级元素用掉——它仍然排在下面 1。所谓“块级元素占满一行”,说的是它 width: auto 时会被拉满,以及它在行方向上是独占的,不是“无论写多宽都铺满”。
第二,上下相邻的两个块级盒子,外边距会 合并(collapse):上面那个有 margin-bottom: 30px,下面那个有 margin-top: 20px,两者之间的实际距离是 30px,取大的那个,而不是相加 1。这是块级格式化上下文里的规则,换成 inline-block 就不合并。
width、height、四个方向的 margin、padding、border 在块级盒子上全部生效——上一篇那套盒模型算法,主要就是为块级盒子准备的。
行内盒子:像句子里的词一样首尾相接
<span>、<a>、<strong> 这些元素的默认 display 是 inline。行内盒子不是从上往下排,而是 沿着文字行走的方向一个接一个,就像句子里的词:排不下就折到下一行,这些折出来的行在规范里叫行盒(line box)4。
关键在于:水平方向和垂直方向的规则不一样。
水平方向,也就是文字往前走的方向,margin、border、padding 都正常生效,会把左右的文字推开 4。
垂直方向则几乎全部失效:
width和height完全不生效——给span设宽高没有任何反应 2;- 上下
margin也 不生效; - 上下
padding和border会 画出来,但不撑开行盒,不会把上下两行文字推开,它们会直接压在相邻行的内容上 4; - 行内盒子的高度取决于字体本身,不是你能用
height去控制的量 2。
一行占多高由 line-height 决定;而某个行内盒子“自己多高”,是字体决定的事,两者不是一回事。同一行里的行内盒子按 基线(baseline,字母底部那条对齐线)对齐,所以把一行里混着不同字号的文字时,行盒会被撑得比预期高。
有一个例外值得单独记住:<img> 默认 display 也是 inline,但它是 替换元素(replaced element),内容来自 CSS 之外、自带固有尺寸,所以 width、height 在它身上照样生效,用起来像 inline-block 7。同样地,input、button、select 的默认值是 inline-block,不是 inline。“行内不能设宽高”这句话,对这些元素不成立。
inline-block:留在句子里的块
想放一排卡片,天然会想到:既想让它们并排,又想让每个都能设宽高、有四边内边距。display: inline-block 就是为这个场景准备的。
按规范的定义,inline-block 等于 inline flow-root 3:外层是 inline,所以它 留在文字流里,不会另起一行,像词一样跟着前面的内容排;内层是 flow-root,也就是它内部自己是一套完整的块级布局。于是——2
width、height生效;- 四个方向的
margin、padding、border都生效,会把周围的元素推开,而不是像纯inline那样上下重叠; - 它 建立了一个独立的格式化上下文,所以它的外边距不会和外面合并,内部的浮动也不会漏出去 5。
一句话概括:inline 的排位,block 的内部。但它并排出来的效果,几乎总会让人踩到三个坑。
第一个坑是缝隙。 HTML 里标签之间敲的回车和缩进会被当成一个空格,而在行内上下文里,一个空格就是宽度真实的词间空格。所以写成:
1<div class="card">A</div>
2<div class="card">B</div>这两个 inline-block 之间会多出一小段空白。常见的说法是约 0.25em,在 16px 字号下差不多 4px,但具体宽度取决于字体 6。这不是 bug,就是“空格之间的空格”被如实渲染了。常见绕法有三种:把标签贴在一起写或塞注释、给父元素设 font-size: 0 再在子元素上把字号设回来、用负 margin 顶回去 6。它们的共同点是都在 抵消 一个你没打算要的东西,而且负 margin 的具体数值还得看字体,不同字体算出来不一样 6。
第二个坑是基线对齐。 inline-block 默认按基线对齐:盒子里有文字时,按最后一行文字的基线对齐;盒子里没有文字内容时,基线取盒子的底边。图片放在一行文字里时,图片按自己的底边对齐文字的基线,而基线和行底之间还要留给字母下伸的部分(比如 g、y 的下半截),图片下方于是多出一条缝。要清掉它,得靠 vertical-align 手动纠正。
第三个坑是宽度和分行不受你控制。 想让一排卡片等宽并铺满一整行,你得自己算百分比,还要把那点缝隙的宽度减掉;卡片多了换行时,每行放几个、剩余空间怎么分配,只能看它自然折行,没办法让它们均匀分布或者在行内居中。
这三个坑是同一个原因:inline-block 的排布规则来自行内上下文,而那个上下文是为“排文字”设计的,不是为了排一排布局用的方块。你想管的是这一排怎么分、间距多少、上下怎么对齐,它关心的却是这些方块像词一样怎么摆。
这就是 Flexbox 要接手的地方
display: flex 把这两件事拆开:容器自己的外层类型仍然是 block,所以它还是独占一行;但它建立一个 flex 格式化上下文,直接子元素不再走块级和行内那套规则,而是变成 flex item,由容器统一决定方向、间距和对齐 1。
换成 flex 之后,前面三个坑都不需要绕了:缝隙问题不存在了(flex item 之间没有空白文字参与),盒子之间要等距有 gap,水平方向怎么分配有 justify-content,垂直方向怎么对齐有 align-items,多行怎么排有 flex-wrap。以前用 inline-block 加手算百分比加缝隙 hack 拼出来的东西,变成容器上的一两条声明。