写在前面
margin的合并规则算是CSS盒模型里最复杂部分,没有之一。因为这部分内容涉及很多不太容易理解的概念,例如clearance(间隙)、normal flow/in-flow(常规流)、BFC(块格式化上下文)、line box(行框)、inline box(行内框)、bidi(双向环境)等等
CSS盒模型不只是7项水平属性 + 7项垂直属性:
margin
border
padding
width/height
P.S.想起高跟鞋的梗——“不仅有padding,今天还加了margin”
相关的内容至少还包括:
context-box
与border-box
padding/margin
百分比的计算方式background
与padding/margin/border
margin
负值margin
合并
盒模型是视觉格式化模型中的基础单元,是CSS布局模型中必不可少的一部分
CSS盒模型描述了一个为文档树中的元素生成的并根据视觉格式化模型进行布局的矩形框
(引自8 盒模型)
所以,盒模型也是CSS在文档树之上建立的第一层抽象,是CSS布局控制与文档元素直接关联的部分。而外边距合并是直接影响垂直格式化的因素之一,有必要深入理解
一.经典场景
下列例子中,假设UA没有默认样式表,未声明的样式属性都依照规范取其初始值
另外,假设UA都是遵守CSS规范的
1.列表项间的外边距合并
li {
margin: 8px;
}
那么列表项之间的间距是多少?
.li-case1 li {
margin: 8px;
/* 添个上内边距 */
padding-top: 1px;
}
.li-case2 li {
margin: 8px;
/* 添个下边框 */
border-bottom: 1px solid;
}
在case1和case2中,列表项间距分别是多少?
2.深层嵌套的外边距合并
/* 缩进表示对应文档结的构嵌套关系 */
div.outer,
div.container,
div.content,
div.inner {
margin: 10px;
min-width: 100px;
min-height: 100px;
}
这4个嵌套的div渲染结果是什么样子?
div.outer,
div.container,
div.content,
div.inner {
margin: 10px;
min-width: 100px;
min-height: 100px;
/* 添个border */
border: 1px solid;
}
现在呢?
div.outer,
div.container,
div.content,
div.inner {
margin: 10px;
/* 删掉min-width, min-height和border */
}
那么现在呢?
3.带间隙的外边距合并
div.container {
border-top: 1px solid;
background: #ccc;
margin-bottom: 60px;
}
/* 缩进表示对应文档结的构嵌套关系 */
div.float {
float: left;
width: 100px;
height: 50px;
}
div.following-float {
clear: left;
margin-top: 50px;
}
div.following-container {
color: red;
}
红色文本顶端距.following-float
底端的距离是多少?
div.container {
border-top: 1px solid;
background: #ccc;
margin-bottom: 60px;
}
/* 缩进表示对应文档结的构嵌套关系 */
div.float {
float: left;
width: 100px;
height: 50px;
}
div.following-float {
clear: left;
/* 把50改成49 */
margin-top: 49px;
}
div.following-container {
color: red;
}
现在呢?
再把50
改成0
和51
呢?又分别会出现什么情况?
P.S.这些问题的答案此刻还是未知的,因为Demo还没开始写;-)那么就有了足够的时间容我们认真猜一下
二.合并条件
什么样的外边距会发生合并?
水平外边距不合并。相邻的垂直外边距会合并,除了2种特殊情况:
根元素盒的外边距不合并
如果一个带有间隙的元素的上外边距与下外边距相邻,它的外边距会和紧挨着的兄弟(元素)的相邻外边距合并,但合并后不会再和父级块的下外边距合并
第1条跳过,对根元素应用外边距不在情理之中
第2条引入了一个新概念,叫“间隙”,英文名clearance,看样子与clear
属性有关,实际符合直觉,是指clear
属性导致元素位置移动形成的间隙,见CSS规范9 视觉格式化模型。隐含两个关键点:
具有
clear
属性并且(
clear
属性)让元素位置发生移动了
如果满足这两个条件,就说一个元素带有间隙
注意:如果应用了clear属性,元素的实际位置不变,比如通过margin-top
把元素放到那个位置的,此时元素自身的布局位置与clear
效果位置一样(即clear
属性没有带来额外的空间占用,所谓的间隙),就不具有间隙。反过来,如果应用clear
属性,导致元素的实际位置发生了变化,即元素上方有一部分空间是clear
属性带来的,那么就算带有间隙
带有间隙还不够,还要该元素的上下外边距相邻(意味着元素的实际高度为0,且没有padding, border
),同时满足的话,这个元素的外边距合并会受到限制:其外边距只和紧挨着的兄弟的相邻外边距合并,合并后的结果不会再和父级块的下外边距发生合并
P.S.到这里有挑战经典场景3的入场券了,但还差得很远
“相邻”的定义
两个外边距在什么情况才算“相邻”?
都属于流内(in-flow)块级盒,处于同一个块格式化上下文
没有行框(line box),空隙,内边距和边框把它们隔开
都属于垂直相邻框边界(vertically-adjacent box edges)
3句话4个新概念,深度优先过一下
流内
流内/流外(in-flow/out-of-flow)是指是否用常规流定位方案来布局该元素
继续深度优先,定位方案分3种:
常规流。包括块格式化、行内格式化和相对定位
浮动。从常规流的位置取出来向左/右移
绝对定位。从常规流中脱离出去,根据其包含块确定自身位置
元素既没有浮动(float
属性的应用值为none
),也没有绝对定位(position
属性的应用值不为absolute
),并且不是根元素,那就按常规流来布局,就属于流内元素,否则就是流外元素
块格式化上下文
浮动,绝对定位的元素,非块盒的块容器(例如inline-blocks,table-cells和table-captions),以及’overflow’不为’visible’的块盒(当该值已被传播到视口时除外)会为其内容建立新的块格式化上下文
在一个块格式化上下文中,盒在竖直方向一个接一个地放置,从包含块的顶部开始。两个兄弟盒之间的垂直距离由’margin’属性决定
也就是说,如果没人建立新的BFC,那么就处于当前BFC。像JS作用域一样,默认大家都位于最外层作用域(最外层块格式化上下文),遇到普通块级盒就放进块格式化上下文,遇到特殊的(浮动,绝对定位的等等)就新建一层作用域(建立新的块格式化上下文),它里面的元素都放进这个内层作用域(新的块格式化上下文)
布局完成后从格式化上下文的角度来看,就是一系列嵌套的BFC,每个BFC负责管理一组块盒(或者说块级元素)的布局
注意:这里不提行内格式化上下文,因为区分出不同的行内格式化上下文没有太大意义(规范定义中,没有关于跨行内格式化上下文的特殊场景)。那么,什么时候会创建新的行内格式化上下文?,根据规范,只在块容器只含有行内级盒时才创建一个新的行内格式化上下文,不像BFC可以显式地强制创建
P.S.关于何时会创建新行内格式化上下文的更多讨论,请查看When does a box establish an inline formatting context?
行框
包含来自同一行的盒的矩形区域叫做行框
一个行框总是足够高,能够容纳它包含的所有盒。
行框是CSS对行的抽象表示,每行元素都处于同一个行框里。如果太长放不下出现自动换行,那么就会为下一行再创建一个行框。另一方面,行框不是纯粹的抽象定义,它具有宽度和高度,用于决定行布局
相邻外边距之间“没有行框”可以简单理解为没有行内元素把它们隔开
垂直相邻框边界
下列4种场景满足外边距都属于垂直相邻框边界的情况:
盒的上外边距与其第一个流内(in-flow)孩子的上外边距
盒的下外边距与其下一个流内紧挨着的兄弟的上外边距
最后一个流内孩子的下外边距与其height计算值为’auto’的父元素的下外边距
盒的上外边距和下外边距,要求该盒没有建立新的块格式化上下文,并且’min-height’计算值为0,’height’计算值为0或’auto’,还没有流内孩子
看起来太长,我们简化条件,假设都是流内元素的话,那么:
父子:父元素上外边距与长子上外边距
兄弟:元素的下外边距与右兄弟的上外边距
父子:幺儿的下外边距与父元素的下外边距
自身:0高“真空”元素的上外边距与下外边距
P.S.这里的“真空”是指——把薯片抽成真空。要么里面什么都没有,要么流内孩子都被抽离了
也就是说,“相邻外边距”的位置定义具体分3种情况:父子,兄弟和自身(自身上下外边距合并是比较奇特的)
重新理解“相邻”与外边距合并
有了前面的概念铺垫,现在我们把零散的点整合起来,先重新定义“相邻”:
父子,兄弟或元素自身的外边距紧挨在一起就是“相邻”
还有一个关键点:紧挨。就是说这两个外边距没被“墙”隔开,“墙”分3种:
种族:双方必须都是流内块级盒
信仰:处于同一个块格式化上下文
地域:二者之间没有行框(line box),空隙,内边距和边框
到这里,“相邻”已经很清楚了,我们再反推外边距合并的定义:
非根元素的相邻垂直外边距会合并,带有间隙的话,合并受限
受限是指带有间隙元素自身上下边距相邻的话,只能与兄弟元素的外边距合并,无法和父元素的下外边距合并
三.合并条件推论
根据外边距合并的发生条件,有8条推论:
浮动的盒与任何其它盒之间的外边距不会合并(甚至一个浮动盒与它的流内子级之间也不会)
建立了新的块格式化上下文的元素(例如,浮动盒与’overflow’不为’visible’的元素)的外边距不会与它们的流内孩子合并
绝对定位的盒的外边距不会合并(甚至与它们的流内孩子也不会)
内联块盒的外边距不会合并(甚至与它们的流内孩子也不会)
流内块级元素的下外边距总会与它的下一个流内块级兄弟的上外边距合并,除非该兄弟(元素)具有间隙
流内块级元素的上外边距会与它的第一个流内块级孩子的上外边距合并,条件是该元素没有上边框和上内边距,并且其孩子不具有间隙
一个’height’为’auto’并且’min-height’为0的流内块级盒的下外边距会与它的最后一个流内块级孩子的下外边距合并,条件是该盒没有下内边距和下边框,并且其孩子的下外边距没有与具有间隙的上外边距合并
盒自身的外边距也会合并,条件是’min-height’属性为0,既没有上下边框,也没有上下内边距,’height’为0或’auto’,且不含行框的话,那么其所有流内孩子的外边距(如果存在的话)都会合并
简化总结,不过4条:
非流内(绝对定位或浮动)不合并
触发新BFC创建(浮动,绝对定位元素,非块盒的块容器以及’overflow’不为’visible’的某些块盒)不与孩子合并
非块级盒(内联块)不合并
一般情况下,兄弟元素的下上外边距,父子元素的上外边距、下外边距,元素自身的上下外边距会合并
前3点针对“相邻”的前提条件(流内,同BFC,块级盒),第4点是对4种“相邻”场景的转述,展开就是8条推论
四.合并行为
两个相邻外边距发生合并后,形成的外边距叫折叠外边距
P.S.collapsed margin故意译作折叠表示结果,与合并的动作区分开
外边距合并有2个特点:
递归:即深层合并。合并一次后,再检查与合并结果相邻的外边距有没有能合并的,有的话接着合
贪婪:追求最宽合并结果。两个margin正值取最大值,两个负值取绝对值的最大值
对于递归特性,“相邻”的定义扩展出一条递归公式:
折叠外边距也能与另一个外边距相邻,只要其外边距的任意一部分与那个外边距相邻就算
贪婪与外边距合并结果计算方式有关,因为margin允许负值,情况稍微复杂一点:
都是正值,直接求二者最大值
一正一负,相加求和
都是负值,求二者绝对值的最大值
例如:
ul {margin-bottom: -15px;}
/* 缩进表示对应文档结的构嵌套关系 */
li {margin-bottom: 20px;}
h1 {margin-top: -18px;}
那么h1
与最后一个li
的垂直距离为20 + -max(|-15|, |-18|) = 2px
无论对正值还是负值,求最大值的原则都是让合并结果尽量宽(绝对值更大的负值能让元素内容偏移出去更远的距离),即贪婪性
五.在线Demo
Demo地址:http://ayqy.net/temp/margin-collapse.html
P.S.答案都在Demo里,解释都在源码里
参考资料
When does a box establish an inline formatting context?:问题评论很有价值,有助于理解行内格式化上下文
Margin collapse and clearance:clearance的示例
带有间隙的外边距合并示例:要用Firefox看,因为Chrome和Safari不遵守规范
Collapsing Margins:看了本文就不用看这个了