先记住这个答案
margin 的百分比,不管是 margin-top、margin-bottom 还是左右两边,都相对包含块的宽度计算,padding 也是一样。规范的说法是相对包含块的行内方向尺寸,横排文字时就是宽度。原因是循环依赖:父元素的高度默认是 auto,要等子元素的 margin 和内容算完才知道;如果 margin-top 再按父元素高度算,两边就互相等待,没法得出结果。宽度一般是从外往里确定的,先有父宽再有子宽,所以拿宽度当参照是安全的。
- 四边百分比边距都参考包含块宽度
- 原因是父元素
auto高度依赖子元素,按高度算会循环 - 竖排写作模式下改为相对包含块的高度
- 等比盒子可直接用
aspect-ratio
先看数字
父元素宽 400px、高 100px,子元素写 margin-top: 50%。很多人会猜 50px,实际是 200px,子元素被推到了父元素外面。
margin 也按宽度算CSS.parent {
width: 400px;
height: 100px;
display: flow-root; /* 防止子元素的 margin 和父元素折叠 */
}
.child {
margin-top: 50%; /* 400 × 50% = 200px,不是 100 × 50% */
padding-top: 25%; /* 同样按宽度算:100px */
margin-left: 10%; /* 40px */
}
/* 用开发者工具看 .child 的盒模型:
margin-top 200px、padding-top 100px、margin-left 40px */三个百分比的参照都是父元素的内容宽度 400px,父元素的 height: 100px 完全没参与。父元素加 display: flow-root 是为了创建 BFC,不然子元素的 margin-top 会和父元素发生外边距折叠,看起来像是父元素被往下推,干扰判断。
- 包含块不一定是父元素
- 普通流里的元素,包含块是最近的块级祖先的内容区,通常就是父元素。
position: absolute的元素,包含块是最近的定位祖先的 padding 盒,这时margin百分比要按那个祖先的宽度加左右padding来算。所以说「相对父元素宽度」不够严谨,说「相对包含块宽度」才准确。
规范为什么这样设计
浏览器布局时,宽度大多是从外往里算的:视口宽度确定了 body 宽度,body 宽度确定了子元素宽度。高度正好相反,大多是从里往外算的:子元素内容、padding、margin 加起来,才得到父元素的 auto 高度。
- 按高度算会怎样
- 假设
margin-top: 50%按父元素高度算。父元素高度是auto,要先知道子元素的margin-top才能算出来;子元素的margin-top又要先知道父元素高度才能算。这就是一个死循环。选宽度做参照,就绕开了这个问题。统一四个方向都用同一个参照,也让margin: 5%这种简写在四边得到相同的值,结果更可预测。 - 竖排文字时反过来
writing-mode: vertical-rl时,行内方向变成了竖直方向,块方向变成了水平方向。这时尺寸从外往里确定的是高度,所以margin和padding的百分比改为相对包含块的高度。规范里一直说的是「行内尺寸」,横排时恰好等于宽度而已。
这个特性的实际用途
因为 padding-top 百分比按宽度算,以前做固定比例的容器(视频封面、图片占位)都靠它。现在有了更直接的属性。
- 从
padding-top技巧到aspect-ratio - 老写法是容器
height: 0; padding-top: 56.25%,高度就等于宽度的 9/16,内容用绝对定位铺满。现在直接写aspect-ratio: 16 / 9,不需要额外的绝对定位,内容也能正常参与布局。新项目优先用aspect-ratio,老代码里看到 56.25%、75% 这类数字,就是在做 16:9 和 4:3 的比例。
容易答错的地方
- 以为上下方向按高度、左右方向按宽度
- 这是最常见的错误答案。
margin和padding四个方向用的是同一个参照。真正按高度算的是top、bottom这些定位偏移属性,以及height、max-height这些尺寸属性。 - 以为给父元素设了固定高度就会按高度算
- 父元素写死
height: 100px也不影响结果。规范规定的是参照方向,不会因为高度已知就换参照。 - 把
translate百分比和margin百分比混为一谈 transform: translateY(-50%)的百分比是相对元素自身的高度,不是包含块。所以垂直居中常写top: 50%加translateY(-50%):前者按包含块高度往下移一半,后者按自身高度往回拉一半。
面试官还会怎么问?
Flex 子项的 margin 百分比也是按宽度吗?
按现行规范也是相对包含块的行内尺寸,也就是 Flex 容器的宽度。早年的规范草案允许 Flex 和 Grid 子项的上下 margin 相对容器高度,不同浏览器实现过不一致,后来统一了。如果项目要兼容很老的浏览器,这里可能遇到差异。
怎么让元素的上边距等于父元素高度的一半?
用 margin 做不到。可以让父元素 position: relative,子元素 position: relative; top: 50% 或者绝对定位后写 top: 50%。也可以换布局思路,用 Flex 的 align-items 或 Grid 分行来控制位置。
padding 百分比按宽度算,会不会引起循环?
不会。子元素的 padding 按父元素宽度算,而父元素宽度通常不依赖子元素。只有在父元素宽度也由内容决定的场景下,比如父元素是 inline-block 或 float 且没写宽度,才会出现依赖关系,这时浏览器会先按内容算出父宽,再回头解析百分比,有时会让结果和直觉不一样。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。