问题引入
html
<style>
.parent {
font-size: 42px;
line-height: 1.5;
background: lightgray;
}
.child {
display: inline-block;
font-size: 12px;
line-height: 1.5em;
background: coral;
}
</style>
<div class="parent">
<div class="child">123456789</div>
</div>
看上述代码的页面渲染,是否会跟你想象的不太一样?为什么我的 .child 明明没有 padding 和 margin,它的周围却有莫名其妙的空白?
如果初看代码,你也觉得整个 .parent 类的高度应该跟 .child 一样,.child 上下的空间是不应该存在的话,那就推荐你认真了解一下 CSS 的 IFC(Inline Formatting Context,行内格式化上下文)和 strut 了。
空白的来源
我们先来看灰色的那一块高度到底怎么来的:
- 父元素
.parent:font-size: 42pxline-height: 1.5- 计算得出父元素的行高为:63px。
- 子元素
.child:font-size: 12pxline-height: 1.5em- 计算得出子元素自身内容的行高为:18px。
所以差距就在于 63 和 18 的差值,但问题是,.parent 里根本没有直接文字节点,里面只有 .child 的文字,为什么还是会按照 .parent 的高度撑起来呢?
Strut 的支撑
因为 .parent 是一个块级容器(<div>),并且它内部包含行内级元素(.child 设置了 display: inline-block),所以 .parent 建立了一个 IFC。
此时如果 .parent 有文字内容,浏览器会在 .parent 的内部,.child 的前面,悄悄塞入一个 strut 。.parent 的 font-size 是 42px,line-height 是 1.5,所以它的高度是 63px。
现在 .parent 里面有两个元素参与排版:
- 高度为 63px 的隐形 strut。
- 高度为 18px 的
.child盒子。
因为两者默认的 vertical-align 都是 baseline(基线对齐),所以浏览器会进行如下操作:
- 找到 strut 的基线。
- 找到
.child的基线(对于inline-block,基线是其内部最后一行文本的基线,这里就是 "123456789" 的基线)。 - 把这两条基线对齐。
然后就会出现你看到的样子,明明 .child 那么小,却会被撑开。这在整个 .parent 里面只有单个元素的场景下看起来有点反直觉,但是试想一下,如果整句话存在多种行内元素的混排,有一个统一的行高是一个再正常不过的设计。
虽然,如果里面存在行内元素高于父元素的行高,还是会被撑起来......
html
<style>
.parent {
font-size: 30px;
line-height: 1.5;
background: lightgray;
}
.child {
display: inline-block;
font-size: 12px;
line-height: 1.5em;
background: coral;
}
.child2 {
display: inline-block;
font-size: 50px;
line-height: 1.5em;
background: coral;
}
</style>
<div class="parent">
这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。
<div class="child">123456789</div>
这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。
<div class="child2">123456789</div>
这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。这是一段很长的内容。
</div>
知道 strut 会造成意外的支撑,那么怎么解决呢?一个比较 hack 的方法是把 .parent 的行高直接压制掉:
html
<style>
.parent {
font-size: 0;
line-height: 0;
background: lightgray;
}
.child {
display: inline-block;
font-size: 12px;
line-height: 1.5em;
background: coral;
}
</style>
<div class="parent">
<div class="child">123456789</div>
</div>
但要说更靠谱的方法,我觉得还是得从消除 IFC 入手。
IFC
因为 IFC 的缘故,Div 里的行高会依照当前 Div 自己的 font-size 和 line-height 产生 strut。那么 IFC 本身是怎么产生的呢?
一句话概括就是:当一个块级容器内部直接包含行内级元素时,这个块级容器内部就会产生 IFC。
如果同时存在块级和行内容器呢?浏览器为了不让文字和块级元素打架,会偷偷创建一个 "匿名块盒" 把文字包裹起来。此时,包含文字的那个"匿名块盒"内部会产生一个 IFC,但最外层的 <div> 整体并不是一个纯粹的 IFC 环境。
我们明白了有 IFC 才会有 strut,那么想要消除这个 strut 只要把格式化上下文改掉就好了。
处理 IFC,CSS 还有 BFC、FFC 和 GFC。
在 CSS 的视觉格式化模型中,除了我们最常听到的 BFC (Block Formatting Context) 和 IFC (Inline Formatting Context) 之外,还有 FFC 和 GFC。
总共加起来,CSS 中主要有以下四种格式化上下文:
BFC (Block Formatting Context,块级格式化上下文)这个大家应该最熟。只要 float 不为 none、position 是 absolute 或 fixed、display 用了 inline-block / table-cell / flex / grid / flow-root(现代最常用),或者 overflow 不为 visible,就能触发。它的特点是内部元素垂直排列、外边距不会和外部重叠、能包住浮动元素,也不会被浮动元素盖住。清除浮动、防 margin 塌陷、自适应两栏布局,都是它的活。
html
<style>
.parent {
font-size: 42px;
line-height: 1.5;
background: lightgray;
}
.child {
display: block;
font-size: 12px;
line-height: 1.5em;
background: coral;
}
</style>
<div class="parent">
<div class="child">123456789</div>
</div>
把 .child 改成 block,里面的文字就根据它自己产生 IFC,问题解决。
FFC (Flex Formatting Context,弹性格式化上下文),display 设成 flex 或 inline-flex 就有。容器里的子元素会变成 flex item,不再按块级、内联那套规则排版,主轴和交叉轴上的空间分配、对齐都比较自由。适合导航栏、卡片居中、均分剩余空间这类一维布局。
GFC (Grid Formatting Context,网格格式化上下文),display 设成 grid 或 inline-grid 就有。容器会被切成行和列,形成网格单元,子元素想放哪就放哪,跨行跨列也随意。整个页面的主体结构、仪表盘这类二维布局,基本就得靠它。
html
<style>
.parent {
display: grid;
font-size: 42px;
line-height: 1.5;
background: lightgray;
}
.child {
display: inline-block;
font-size: 12px;
line-height: 1.5em;
background: coral;
}
</style>
<div class="parent">
<div class="child">123456789</div>
</div>
把 .parent 设置成 flex 或者 grid 之后,子元素也不成 IFC 了,问题解决。
太长不读
strut 因为父元素开了 IFC 才存在 ,它本质上是一个看不见的"文本行",高度完全由父元素自己的 font-size 和 line-height 决定。只要父元素里有行内级内容,把那一行支撑起来,哪怕你里面只放了一个小小的文字,行高也会比你预期高出一截。
所以再遇到这种"莫名的留白"问题,可以首先怀疑是 IFC 的问题,对应修复方法是:
font-size: 0; line-height: 0压制 strut- 让子元素变成
display: block,它会自己内部重新建 IFC,不再使用父元素的 IFC - 或者干脆把父元素改成
flex/grid,子元素变成 flex item / grid item,绕开 IFC