本文整理于:(布局和包含块 - CSS:层叠样式表 | MDN (mozilla.org))。
同时加上了自己的理解,并重新组织了介绍的顺序,补充了相关的内容。并且把行高、字体大小的百分比也进行了归纳介绍。绝对值得一读。
一个盒子中,需要进行百分比计算时,基准并不是上一级包裹的父盒子,而是其所在包含块的那个盒子。
也就是说,元素的width、height、top、bottom、left、right、margin、padding取值为百分比时,它们的计算基数都取决于:
-
它的父级链中,谁可以作为它的包含块(containing block,一个块级、行内块元素、格式上下文)。
-
子元素的定位情况,会决定其在包含块上获取计算基数时,是否要把padding加进来。
如果子元素定位为
static
(没有定位也是定位的一种)、relative
或sticky
,计算基数有两个:分别是包含块的width、height。如果子元素定位为absolute、fixed,计算基数有两个:包含块的width+左右padding、height+上下padding。
关于为什么定位为absolute、fixed时,要算上padding的个人记忆:
定位为absolute、fixed的子元素设置top、left属性设置为0时,会覆盖定位父盒子的padding,但不覆盖父盒子border,也就是把padding也当成父盒子“活动”区域了。而其它定位不会覆盖父盒子padding,计算基数自然只有width、height区域。
另外,父盒子box-sizing为border-box时,padding和内部真实width、height会一起分配css中设定的width、height值。在css中计算时,直接算css中的width或heitht即可,因为它包含padding了。
如何找到包含块
首先,一个可以成为包含块的元素,必须是非行内元素,可以设置宽高。如一个块级、行内块元素,或是一个格式化上下文。
然后,子元素的position定位属性,会决定其选取哪个元素作为包含块,有三种普通情况,一种特殊情况。
普通情况:
-
当子元素的定位为static(等于没设置)、relative、sticky,它的包含块为最近的非行内父盒子。
计算基数为包含块的:width、height(不含父盒子padding)。
-
当子元素定位为absolute,它的包含块为上级链中,定位不为static的盒子。
计算基数为包含块的:width+左右padding、height+上下padding。
-
当元素定位为fixed,它的包含块为当前视口(
100vw
、100wh
的区域)。计算基数为视口区域:
100vw
、100wh
。-
和文档的
scrollWidth
、scrollHeight
没关系。 -
需要注意的是,在显示设备中(屏幕),称为连续媒体,包含块是浏览器的视(窗)口区域。在pdf、打印机中,称为分页媒体,包含块是一页纸的区域。
-
特殊情况:
子盒子定位为absolut、fixed,其定位父盒子却是static定位元素的情况。
一般情况下,当子元素定位为absolute,进行定位时,会找父级链中定位属性不为static的盒子定位。当子元素定位为fixed,定位以视口区域进行定位。
-
但是当子元素的定位定位为absolute、fixed的情况下,还没找到满足上述条件的父盒子前,存在有以下特殊属性的更近的父盒子,记为A,则元素的包含块会变成A,并且定位也是相对于A定位。
absolut、fixed的定位的子元素,其定位父盒子可以是包含以下特殊属性的盒子,而不用关心父盒子的定位情况。
transform
或perspective
的值不是none
。(例如:perspective: 1px;
)will-change
的值是transform
或perspective
filter
的值不是none
或will-change
的值是filter
(只在 Firefox 下生效)。contain
的值是paint
(例如:contain: paint;
)。backdrop-filter
的值不是none
(例如:backdrop-filter: blur(10px);
)。
这种情况下,计算基数依旧是包含块(上面提到的A元素)的:width+左右padding、height+上下padding。说白了就是可选取的定位父盒子(包含块)范围变宽了,可以把上面普通情况的第二种情况,整理为:
- 当子元素定位为absolute,它的包含块为上级链中,找到第一个包含上面特殊属性的父盒子、或定位不为static的盒子作为包含块。
- 当子元素为fixed,它的包含块为上级链中,找到第一个包含上面特殊属性的父盒子,否则以视口作为包含块。
选取包含块的哪个基数计算
通过以上规则找到包含块后,根据子元素的定位情况就可以找出两个基数了:
- 子元素定位为普通定位:
static
(或没有定位)、relative
或sticky
- 横向的基数:包含块的width
- 纵向的基数:包含块的height
- 子元素定位为:position、fixed(包含块为含有特殊属性盒子)
- 横向的基数:包含块的width + 左右padding
- 纵向的基数:包含块的height + 上下padding
- 子元素定位为:fixed(包含块为视口区域)
- 横向的基数:
100vw
- 纵向的基数:
100vh
- 横向的基数:
以上已经找出了两个方向的基数,但选取哪个基数进行计算呢:
-
选取横向基数的属性:width、left、right、padding、margin
-
选取纵向基数的属性:height、top、bottom
行高、字体百分比
这里引入font-size和line-height是为了呼应题目,font-size的百分比计算确实是受上级包裹盒子的影响。line-height百分比计算则是与自身元素的font-size有关,和包裹盒子并不是绝对关系。和上文、后文主要引出的包含块没有关联。
font-size、line-height是可继承的属性,任何类型的元素都可以继承与被继承,和包含块没关系。和上面的属性属于两个范畴了。
font-size的百分比计算:
-
直接读取上一个包裹元素的font-size(上一个元素的font-size可能也是继承过来的,所以肯定有值),并乘以百分比。
<div style="font-size: 20px;"> <p style="font-size: 150%;"> <!-- 等价于:font-size: 1.5em; --> <!-- p: 20px * 150% = 30px --> 欢迎关注点赞greaclar,一起发现更多技术细节与规律 </p> </div>
line-height的百分比计算:
- line-heitht的百分比计算会读取当前元素的font-size属性(可能是继承过来的),并乘以百分比。
无论是em单位还是百分比,都是读取自身的font-size,在继承上级元素的font-size时,才是拿上一级元素的font-size,但本质还是拿自己的font-size。
<div style="font-size: 30px; line-height: 150%;"> <p style="line-height: 100%;"> <!-- 等价于:line-height: 1em; --> <!-- p:line-height 30px * 100% = 30px --> 欢迎关注点赞greaclar,一起发现更多技术细节与规律 </p> </div>
line-height属性继承的百分比问题:
line-height定义在自身元素上时,按照上面的方式,找到自身的font-size计算即可(可能是继承过来的font-size值)。
如果line-height是继承过来的就比较危险了,有两种情况:
- 继承的源头line-height,它的原始值是百分比或em单位。计算基值是源头line-height所在元素的font-size。
本质是,该line-height在源头元素中就已经被计算出绝对的px长度了,被继承时,已经是绝对值了,不会在有百分比的概念。 - 继承的源头line-height,它的原始值是不带单位的比值。计算基值是继承所在元素自身的font-size。
本质是,继承过来的line-height是原始的比值,还没计算出绝对的px长度。被继承后,需要和宿主的font-size进行计算。所以这种line-height的继承效果,等价于把line-height直接定义在元素自身上。
<!-- 百分比 -->
<div style="font-size: 30px; line-height: 100%;">
<div style="font-size: 40px;">
<p style="font-size: 60px;">
<!-- p:line-height 30px * 100% = 30px -->
欢迎关注点赞greaclar,一起发现更多技术细节与规律
</p>
</div>
</div>
<!-- 数值比值 -->
<div style="font-size: 30px; line-height: 1;">
<div style="font-size: 40px;">
<p style="font-size: 60px;">
<!-- p:line-height 60px * 1 = 60px -->
欢迎关注点赞greaclar,一起发现更多技术细节与规律
</p>
</div>
</div>
例子
本文部分例子来源mdn,并进行了分类。可以去mdn示例查看部分例子的运行截图。
子元素定位为普通定位:static
(或没有定位)、relative
或 sticky
如果子元素的父盒子存在块级元素、行内块元素、格式上下文,则选取最近的符合该条件的父盒子作为包含块
/*<body> <section> <p><p/> </section> </body>*/
body {
background: beige;
}
section {
display: block;
width: 400px;
height: 160px;
background: lightgray;
}
p {
/* 这三种定位加了并不会影响section就是它包含块的事实
position: reative;
position: static;
position: sticky;
*/
width: 50%; /* == 400px * .5 = 200px */
height: 25%; /* == 160px * .25 = 40px */
margin: 5%; /* == 400px * .05 = 20px */
padding: 5%; /* == 400px * .05 = 20px */
background: cyan;
}
如果父元素中,只有行内元素,则直接选body为包含块
/*<body> <section> <p><p/> </section> </body>*/
body {
background: beige;
}
section {
display: inline;
background: lightgray;
}
p {
width: 50%; /* body元素的宽度一半 */
height: 100%; /* body的height为0,这里也为零 */
background: cyan;
}
子元素定位为:position、fixed
子元素定位为position,选取最近定位不为static的父元素为包含块。
- 前提:该父元素和子元素之间没有设置特殊属性的元素
/*<body> <section> <p><p/> </section> </body>*/
body {
background: beige;
}
section {
position: absolute;
left: 30px;
top: 30px;
width: 400px;
height: 160px;
padding: 30px 20px;
background: lightgray;
}
p {
position: absolute;
width: 50%; /* == (400px + 20px + 20px) * .5 = 220px */
height: 25%; /* == (160px + 30px + 30px) * .25 = 55px */
margin: 5%; /* == (400px + 20px + 20px) * .05 = 22px */
padding: 5%; /* == (400px + 20px + 20px) * .05 = 22px */
background: cyan;
}
子元素定位为fixed,包含块为视口区域
- 前提:子元素父级元素中,都没有含有特殊属性的元素
/*<body> <section> <p><p/> </section> </body>*/
body {
background: beige;
}
section {
width: 400px;
height: 480px;
margin: 30px;
padding: 15px;
background: lightgray;
}
p {
position: fixed;
width: 50%; /* == (50vw - (width of vertical scrollbar)) */
height: 50%; /* == (50vh - (height of horizontal scrollbar)) */
margin: 5%; /* == (5vw - (width of vertical scrollbar)) */
padding: 5%; /* == (5vw - (width of vertical scrollbar)) */
background: cyan;
}
absolute、fied子元素,选取含有特殊属性的父元素,如transform: rotate(0deg);
,作为包含块
- 前提:如果是absolute定位的子元素,该父元素和子元素之间只有定位为static的元素
/*<body> <section> <p><p/> </section> </body>*/
body {
background: beige;
}
section {
transform: rotate(0deg);
width: 400px;
height: 160px;
background: lightgray;
}
p {
position: absolute; /*position: fixed;同样效果*/
left: 80px;
top: 30px;
width: 50%; /* == 200px */
height: 25%; /* == 40px */
margin: 5%; /* == 20px */
padding: 5%; /* == 20px */
background: cyan;
}
案例反刍:宽高等比例缩放的盒子
这是一个非常经典的面试题。实现一个高度和宽度等比例缩放(示例中为1:1)的盒子。
如果能看懂这个案例,基本就理解了包含块了,欢迎留言讨论哦。
<template>
<div class="greaclar-father">
<div class="father">
<div class="children"></div>
</div>
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue';
</script>
<style scoped>
.greaclar-father {
box-sizing: border-box; /* 下面定义的width:600px包含左右各50px的padding和500px的实际width */
width: 600px;
height: 400px;
padding: 50px;
perspective: 0px; /*同样可以成为 .father 元素的定位父盒子(包含块)*/
background-color: #e55b5b;
}
.father {
position: fixed;/* 会获取其包含块padding及内部的宽高来计算百分比 */
box-sizing: border-box;
width: 50%; /* 600px * 50% = 300px */
padding-top:50% ; /* 600px * 50% = 300px */
background-color: #bedc46;
}
.children {
position: absolute; /* 会获取其包含块padding及内部的宽高来计算百分比 */
top: 0;
left: 0;
width: 100%; /* 包含块width + 左右padding = 300px + 0 = 300px */
height: 100%; /* 包含块height + 上下padding = 0 + 300px = 300px */
background-color: #5bd440;
}
</style>
包裹盒子无法overflow:hidden截断子盒子
包含块除了可以作为内部子元素的定位参照、百分比计算基数,也是overflow:hidden
唯一可生效于该子元素的父盒子。也就是说,如果子元素(记为X)和其包含块之间,还有中间父盒子,这些非包含块父盒子的overflow:hidden
对于子元素X来说是无效的。
示例解析:
- 以下示例中greaclar-father盒子设置了
perspective: 0px;
,成为positon:absolute
定位的children盒子的包含块。即使father盒子是children的第一个包裹父盒子,也设置了overflow:hiddern
,但children盒子还是会超出father盒子完整显示。
建议:
- 给元素设置overflow时,需要留意内部是否有position属性值为absolute、fixed的子元素。并确保这些子元素的包含块是自己。
- 但是,为了代码的可维护性,最好不要在设置了overflow的元素内部,对子元素进行absolute、fixed定位。
<template>
<div class="greaclar-father">
<div class="father">
<div class="children"></div>
</div>
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue';
</script>
<style scoped>
.greaclar-father {
box-sizing: border-box;
width: 600px;
height: 400px;
padding: 50px;
perspective: 0px; /*含有特殊属性,并作为.children的包含块*/
background-color: #e55b5b;
}
.father {
/*.father 不满足成为.chindrend的包含块条件,无法截断.children*/
overflow: hidden;
box-sizing: border-box;
width: 300px;
padding-top:300px ;
background-color: #bedc46;
}
.children {
/* absolute定位会查找包含特殊属性或定位不为static的父盒子作为包含块 */
position: absolute;
top: 0;
left: 0;
width: 700px; /*长度超出father部分不会被father盒子截断*/
height: 100px;
background-color: #5bd440;
}
</style>