你可能听说过CSS常规流(norm flow)中的inline和block。但是你知道在CSS中block和inline元素的相对位置并不由display属性决定吗?其实际上由formatting context决定,其同时受到兄弟元素的影响。
你可能使用过z-index来固定元素的相对层叠顺序。但是你知道z-index在文档中并不是绝对的顺序,相反其是相对于层叠上下文的?
你可能听过盒子模型。但是你知道事实上存在至少5种盒子模型吗?其区别在于如何处理内容大小和margin:auto属性,读完本文后你就会知道了。
CSS布局学起来很难,你从无数的网站学习到很多布局小技巧但是你却从未理解背后的布局算法。
元素的布局信息事实上由位置大小层叠位置决定,你一旦了解了如何决定这些信息,你就理解了布局算法。
一:CSS的盒子位置
CSS布局的核心在于如何将一系列HTML元素映射到一组盒子上,这些盒子的被放在x-y-z坐标系里。
盒子的x-和y-坐标事实上由布局方案(positioning scheme)决定。本章中,我会详细讨论CSS2.1中引入的positioning scheme:normal flow,floats和absolute positioning。
概念上来说,CSS布局的最高抽象就是positioning scheme。一旦positioning scheme决定了,其可以进一步的被特殊的layout modes修改,这些layout modes包括display:table,display:inline-table。即使在CSS3的扩展中,其引入了其他的layout modes包括flexbox和grid,其仍然处于上述的positioning scheme中(如display:flex和display:inline-flex);
Positioning schemes
CSS2.1定义了三种positioning scheme,其包括:
- normal flow,其包含三种formatting contexts:block,inline和 relative formatting context
- floats:其与normal flow互动,其构成了现代CSS grid framework基础
- absolute positioning,其处理absolute 和fixed元素相对于normal flow的定位
positioning scheme对 x-axis 和 y-axis元素定位有重大影响。所有的元素除非通过设置float和position属性从normal中移除,默认属于normal flow。
通过它们与normal flow元素的交互方式能更好的理解float和absolute positioning,所以我先说明normal flow。
仔细想下,事实上layou存在两方面在起作用:
- 元素的盒子大小和对齐方式,其通常由display的属性(width,height,margin)控制
- 一个特定父元素的子元素是如何相对于彼此放置的
一个父元素的子元素的相对位置是通过父元素为子元素建立的formatting context控制的,在normal flow中,可以使block formatting context或者inline formatting context。
哈哈哈,这里终于到了鼎鼎大名的bfc(block formatting context),简单来说,bfc就是normal flow中的一种formatting context。所以触发bfc这种说法根本来说就是错误的,更为准确的说法应该是建立了一个新的bfc(establish a new block formatting context)。鉴于bfc的迷惑性,在CSS3中已经被改为flowing root了。至于其种种神奇的作用(比如阻止margin collapse)则在于CSS中很多的特性均需要满足一定的前提,而对于margin collapse,其条件是collapsing-margins
- 两者都为处于同一bfc下的 in-flow block-level boxes
- 两者间不存在line boxes,clearance,padding,border
- 都处于垂直相邻的盒子边缘
这里的第一个条件是处于同一bfc下的盒子才可能发生margin collapse,那么假如父元素创建了新的bfc,那么父元素和子元素将不再处于同一bfc下(子元素处于父元素创建的新的bfc下),则可以阻止发生margin collpase,相反若子元素创建了新的bfc,那么子元素和父元素仍然处于同一bfc下,只是孙子元素处于新的bfc下,父元素和子元素仍然发生margin collapse,其效果如下所示
Title .parent{ width: 100px; height: 100px; background: antiquewhite; margin: 10px; } .son{ width: 40px; height: 40px; background: red; margin-top: 50px; } .demo2 .parent{ overflow: hidden; } .demo1 .son{ overflow: hidden; }渲染效果如下
可以看出子元素创建了新的bfc但是并不能阻止margin collapse
所以bfc并不是高深莫测的东西,其和其他特性的结合才变得有趣。
CSS2.1规范定义formatting context如下:
normal flow里的box属于一个formatting context,其是block或者inline,但不能同时都是。block-level盒子参与bfc(block formatting context),inline-level盒子参与ifc(inline formatting context)
父元素(容器)根据子元素是inline-level还是block-level为其子元素建立formatting context。术语inline-leve和block-level用来强调display属性不为inline和block的盒子仍然可以属于这两种formatting context。例如display:table元素被认为是block-level而display:inline-table元素被认为是inline-level。
一个block-level的元素定义如下
block-level元素是那些在文档中视觉表现为block的元素(例如paragraph)。display属性为‘block’,‘list-item'和’table'的元素是block-level
block-level的盒子就是参与bfc的盒子。每个block-level的元素都会生成一个主block-level box,其包含子孙盒子并且生成内容,它也是负责参与positioning scheme的盒子。一些block-level的元素也可能除了主盒子外还生成附加的盒子如‘list-item’元素。这些附加的盒子相对于主盒子放置。
几乎所有的block盒子都是block container。一个block container就是一组其他盒子的parent并且有一个特定的formatting context:
除了table boxes和替换元素,一个block-level的盒子也是一个block container 盒子。一个block container盒子或者仅包含block-level盒子或者创建了ifc即只包含inline-level盒子
一个inline-level元素定义如下:
inline-level元素是那些在源文档中不构成块状内容的元素;这些内容被分散在各行之中(如inline images和text等)。display为'inline','inline-table','inline-block'的元素是inline-level的。inline-level元素生成inline-level盒子,其参与ifc。
一个inline盒子是那些即是inline-level的
。。。。todo
我不会讲替换元素和非替换元素的区别,因为其区别很小。最简单的看待替换元素的方法是把它们当做img或者video元素,即这些元素仅包含一个整体内容(外部环境定义)并且不能像文本内容那样被拆散成多行。
关于上面定义最有趣的地方在于每个盒子包含的formatting context要么是’inline formatting conext"要么是"block formatting context"。这既是说level每个父元素的所有参与布局的孩子元素只能使用其中一种formatting context。当你将如text的inline-level元素和如div的block-level元素混合使用时,这怎么可能办得到。答案就是inline-level元素可以被提升为block-level元素。这个机制就是匿名盒子生成。
Anonymous box generation
匿名盒子生成用于处理当一个父元素即含有inline-level盒子也含有block-level的情形(此时就会生成匿名盒子)和当标签包含了inline-level元素与包围文本的混合,如paragraph里嵌入em和i标签。
Anonymous block boxes
规范里给了如下例子,可得
Some text
More text
如果一个容器盒子(如上面div生成的盒子)内部有一个block-level盒子(如上面的P)那么我们强迫其内部有两个block-level盒子
规范里提供了如下的描述阐明匿名盒子如何包裹inline-level内容

当一个inline盒子包含一个in-flow的block-level的盒子,那么inline盒子(和其同一行的祖先盒子)会被打散围绕着block-level盒子(且任何的block-level兄弟都是连续的或者被collapsible whitespace或者out-of-flow元素分割)
todo
简单来说,当一个inline-level和block-level盒子混杂在一个父元素内部时,inline-level盒子被拆散围绕在block-level盒子并且其内容被匿名block-level盒子包裹。
Anonymous inline boxes
当一个block container包含文本并且这些文本没有被包含在inline-level元素里时会生成匿名内联盒子(anonymous inline boxes)。例如如下标记
Some emphasized text
会生成两个匿名内联盒子,一个包含“Some",一个包含”text“.
匿名盒子生成非常重要,因为其决定了该为在normal flow中包含block 和inline-level的元素生成何种formatting context。许多现实中的HTML布局里的一个父元素既含有block-level也含有inline-level内容。匿名盒子生成可以保证如果任何的block-level被混入inline-level的兄弟元素时,那么inline-level盒子会被包裹进一个匿名block-level容器内,这也意味着它们将被视为block-level元素相对于其他盒子布局。
既然我们知道了如何确定formatting context,那么我们接下来看如何进行布局。
Normal flow positioning
在normal flow中,一个父盒子内的子盒子是按照formatting context布局的。normal flow中的两个formatting context大致对应于垂直排列和水平排列
Normal flow: block formatting
规范为bfc下如何布局提供了很好的说明
在blocking formatting context中,盒子一个接一个,垂直的,自顶向下的排列。两个相邻的盒子之间的距离由由margin属性确定。相邻block-level盒子的垂直的margin可能发生重叠
在block formatting· context下,每个盒子的左外边缘触碰到containing block的左边缘(对于right-to-left formatting· context,右边缘相邻)。这甚至适用于存在floats的情形(尽管一个盒子的line boxes可能由于floats收缩),除非盒子建立了一个新的block formatting context(此时盒子本身可能由于floats收缩)。
下面的demo可以解释上面的话
最重要的两个结论是在bfc下盒子是垂直排列的,而且每个盒子的左边缘会触碰到containing box的左外边(即使在floats存在的情况)
下面的代码何以演示这个规则:
.float {
float: left;
}
.foo {
padding-top: 10px;
}
.bar {
width: 30%;
}
.baz {
width: 40%;
}
float
foo
baz
上面的例子中
- 每个block盒子都在containing box的左外边缘
- 即使存在float也不影响其在左外边缘的位置,除了会造成文本的偏移
- foo对应的block box(没有明确的设置宽度)宽度扩展为容器宽度
- 另外两个设置宽度的盒子,从左边缘扩展
- 另外两个盒子并没有在一行,即使其能够塞进一行
总的来说,bfc比较简单,用几个段落就可说清,但是ifc并非如此
Normal flow: inline formatting(TL;DR )
ifc相当复杂,更复杂的细节以后再补充。。。
ifc比较复杂,因为其涉及到将内容拆分为line boxes,这是一个你不能从标记直接看出来的结构其是根据内容生成的
在ifc中,盒子在containing box里被一个接一个,自上而下的水平放置。水平的margin,borders,和padding散布在这些box之间。
这些盒子可以以不同的方式垂直放置:其底部和顶部可能是对齐的。包含这些盒子的方形区域构成了一行即行框(line box)
line box的宽度由containing box的宽度和是否存在float决定。line box的高度由下节的高度计算决定
简单来说,在ifc中盒子水平的相邻排放。line box按需生成;其宽度通常为包含块的高度(减去floats)而且其高度足够包含其内部所有的盒子。特别的是
一个line box的高度总是足够包含其含有的所有盒子,[...]当几个inline-boxes不能塞进一个line box时,其总是拆分为几个垂直排列的line boxes。因此,一个段落由一系列垂直排放的line boxes组成。line boxes之间没有垂直的分割(除非特别的指明)并且绝不重叠
总之,line boxes的左边缘触及包含快的左边缘且右边缘触及包含块的右边缘。
如果因为存在float导致水平空间缩减那么line boxes宽度上可能有所差异。
同一ifc下的line boxes高度上也常有差异(比如一行可能包含高点的图片而另一行可能只有文本)
当inline box太大以至于无法塞入一行怎么办?这取决于inline box是替换元素还是非替换元素:
当一个inline box的宽度超出了line box的宽度,其将被拆分为几个盒子,并且这几个盒子将散布于各个line boxes。如果一个inline box不能够被拆分,那么这个inline box将溢出line box
当一个inline box被拆分,其margin,border和padding在拆分处并没视觉效果
简而言之,当inline box超过了line box的宽度,其将会尽可能的拆分。当某些规则阻止拆分盒子,那么其将简单的溢出该line box
关于ifc最复杂的情况即line boxe是如何处理对齐的。text-align和vertical-align两个属性控制对齐
Horizontal alignment within line boxes: text-align
text-align属性控制inline-box如何在line box内对齐,
text-align:left则左对齐,right则右对齐
注意到其仅适用于当line box含有空余空间的情形,而且你不能直接控制inline-level box在line box中是如何摆放的,标准规定:
当inline-level box的宽度小于所在line box的宽度时,其在line box的水平分布取决于‘text-align'属性。如果该属性的值为'justify',那么UA(user agent)可以伸展inline boxes内的空格和文本。
换句话说,text-align属性仅在inline content的内容已经分布在line boxes之后才加以应用
一块文本就是一组line boxes。'left','right'和'center'指明了inline-level盒子在line box中是如何对齐的,其对齐是相对于line box而非viewport。对与‘justify’,该性质表明inline-level盒子如果可能就尽可能通过扩展或缩小inline box的内容将其扩散至line-box的两侧,否则就按初始值对齐
通常来说,空白会受到justify属性影响,然而
如果一个元素'white-space'的计算值(computed value)为'pre'或者'pre-wrap',那么该元素的glyph或者空白都不受justify的影响
Vertical alignment within line boxes: vertical-align
下面两个属性控制line box的垂直对齐
vertical-align:默认值为baseline,控制盒子的垂直对齐,仅适用于inline(和table-cell)盒子
line-height:默认值为normal,规定了用于计算line box height的height
vertical-align控制了line box内inline boxes的垂直对齐方式,而非line box自身的对齐方式。为了理解inline box是如何定位的,你需要首先理解line box和inline box的高度是如何计算的。
line box 的高度收到下面两个因素影响:
- 其包含的inline box的高度
- 其包含的inline box的对齐方式
inline box(非替换元素)的高度按下面方式决定
‘height'属性并不其起作用,内容区的高度是基于font的,但是规范没有规定是如何基于font的。UA可以例如使用em-box
inline(非替换元素)垂直的padding,border,margin与inline box的line-height的计算无关。但是当计算line-box的高度时只有line-height起作用。
正如你从规范所见,inline box的 的height由'font'和'line-height'。特别的是,每个font都必须定义一个baseline,一个text-top和一个text-bottom位置。inline box内容区域的高度等于font的高度乘以line-height的属性值
对于一个非替换 inline 元素,'line-height'规定了用于计算line box 高度的height
'line-height'的值可以是绝对值也可以是相对于font height的相对值。其与父元素的高度并不相关,即使其值为百分数,其值可以取值如下:
normal:用户代理定义的一个值,其值为相对于font的相对值,建议normal的取值为1.0到1.2之间。
length:绝对值用于计算line box的height,负值非法。
number:相对值,相对于元素的font size,不能取负值
percentage:相对值,相对于元素的 computed font size,不能取负值
如果一个inline box里包含了多个font怎么办?
如果存在多个font,内容区的高度计算并未在规范中定义。但是我们建议内容区高度的计算应该对于(1)em-boxes (2) the maximum ascenders and descenders, of all the fonts in the element
规范并未定义该值,但是建议其高度能够容纳所有使用的字体
inline box的对齐方式由属性'vertiacal'确定,其属性分为两组,第一组相对于父元素font的baseline,content area,或者是字体相关的sub和super,包括如下属性值
baseline:
middle:
sub:
super:
text-top:
text-bottom:
percentage:
length:
第二组相对于父元素的line box,这是一个递归算法,因为line box的height取决于vertical alignment而vertical alignment又取决于 line box。遇到这种互相递归的算法,肯定需要一个递归基,该算法的递归基就是第一组属性。这也意味着第二组属性值的元素的布局是在第一组属性值元素布局之后。第二组属性包括
top:
bottom:
总结来说inline box有如下属性:
- font size:决定了字体的大小
- line height:决定了inline box的高度
- baseline:其由font确定
而line box的高度计算步骤如下
- 计算line box内的所有inline boxes的高度
- 对所有vertical-align属性值为第一组的元素,应用vertical-align的属性值进行对齐
- 对所有vertical-align属性值为第二组的元素,应用vertical-align的属性值进行对齐
对于line box有如下属性:
- font size:继承自父元素
- height:由line box内的inline box的height 和 alignment确定
- baseline:通过'struct'定义:这是一个不可见,零宽的inline box,其含有font 和line height属性。
一般来书父元素和子元素的font size是一样的,因此看不出line box基线和inline box的基线差别。
注:table也有vertical-align属性,但是这个属性的工作方式完全不同,应该把其与这里的vertical-align区分开来
Normal flow: relative positioning
既然我们已经讨论完bfc和ifc了,接下来可以讨论最后一个normal flow 布局 relative positioning了。
Relative positioning被认为是normal flow布局的一部分,因为其与normal flow没有太大差异。
relative:盒子的位置完全基于normal flow计算,然后对盒子进行偏移。'position:relative'的属性对于table-row-group,table-header-group,table-footer-group,table-row,table-column-group,table-column,table-cell,和table-caption元素是未定义的。
注:我发现理解relative positioning的最好方式是从渲染的角度考虑,我们发现'position:relative'属性几乎能和其他所有布局方式同时配合使用,而不会发生冲突,而position:absolute和float却不行(这两者实际发生冲突),原因在于position:relative的作用是发生在paint阶段,而其他布局则是发生在layout阶段。position:relative是在layout阶段完成后,在paint阶段进行偏移。(这只是一种理解方式很可能有错误)
可以看出子元素创建了新的bfc但是并不能阻止margin collapse