网上关于CSS清除浮动的文章围起来简直可以绕地球两圈。原因是大多数码农总会有一种错觉,总感觉自己写的代码比别人的更加清晰易懂,一言不合就重构,我也一样。

一.为什么要清除浮动

浮动会造成元素脱离普通流,此时普通流中的元素会当做浮动的元素完全不存在一样,这会造成一些布局上的问题,其一是普通流中的元素会被浮动的元素覆盖(文本内容不会被覆盖),其二是它的容器不再会根据它的高度来调整自身的高度,下面的例子演示了这两个问题:

按下按钮查看清除浮动对布局的影响

我是容器
div1 我浮动了
div2 我是普通流中的块级元素 我的宽度应该是100% 我的文字没有被覆盖

上面的例子中由于div1的浮动,div2的一部分被浮动的元素覆盖,容器的高度比它里面浮动的元素高度还要低。

二.使用clear属性清除浮动

清除浮动的方式有千千万,但其思路一共就两种,其一是使用CSS的clear属性,clear属性规定元素的哪一侧不允许出现浮动元素,在CSS1,CSS2中浏览器自动为带有clear属性的元素添加额外的margin-top,从而保证该元素不会与浮动的元素重叠在一起,CSS2.1规范改成在clear元素的外边距之上添加清除空间,因此不会改变元素的外边距,但无论哪种实现,其目的都是为浮动的元素留出足够的垂直空间。在应用clear属性之前普通流中的元素表现得就像浮动元素不存在一样,而应用该属性后则能“感受”到浮动元素的高度,最后完成清除浮动。
现在可以回头再看看上面的例子,查看网页源代码会发现js动态的为div2指定了clear属性,div2感知到了div1的存在从而解决了元素重叠的问题,由于div2处于普通流,容器必须扩展自身的高度才能够包裹住div2,这又解决了第二个问题。现在的问题是这种方式需要div2的辅助,才能清除div1的浮动,而实际应用中有可能浮动的元素本身就是容器的最后一个元素,换句话说实际应用中可能没有div2的存在,此时如何处理容器高度坍塌的问题?
简单粗暴的方式是人为的在浮动的元素后添加一个没有意义的空元素,并为该元素指定clear属性,这就是清除浮动代码的第一个版本,代码如下:

1
2
3
4
<div class="container">
<div style="float:left">我浮动了</div>
<div style="clear:both"><!--我用来清除上面的浮动--></div>
</div>

上面的代码有两个问题,一是添加无意义的标签给开发与维护带来麻烦,二是违反了web开发的原则,即html用来决定网页的内容,css用来决定呈现的样式。上面的代码添加了一个html元素却仅仅用来改变呈现的样式。于是大家顺其自然的想到使用css为容器添加一个:after伪元素,代码变成了下面这样:

1
2
3
4
5
.clearfix:after{
content:"";
display:block;
clear:both;
}

1
2
3
4
<div class="container clearfix">
<div style="float:left">我浮动了</div>
<!--这里会有css创建的伪元素,用来清除上面的浮动-->
<div>

至此,这个浮动清除类(clearfix)已经足以应付大多数情况了。但仍有修改的余地。

三.使用块级格式化上下文(Block formatting contexts)清除浮动

1.BFC有什么用

CSS2.1开始引入BFC的概念,它是KFC的兄弟品牌,额,不好意思脑袋岔路了,通俗的讲BFC指的是页面中的一块渲染区域,它有自己独立的渲染规则,区域中的元素布局不会受到外部影响,也不会影响外部元素。英语基础好的同学还可以点开链接阅读w3c官方对Block formatting contexts的定义,然后你会发现它几乎没什么实质性的内容,这里强烈推荐YUI团队博客的Block Formatting Contexts这篇文章,它清晰的解答了BFC在实际开发中起什么作用:

  • BFC决定了元素间是否会发生垂直外边距叠加
    只有处于同一上下文中的块框才有可能会发生垂直外边距叠加,参考下面的例子:

    p1 我有10px的margin

    p2 我有10px的margin

    p3 我也有10px的margin,但我与楼上二位不在同一BFC

    上例中p1,p2都有20px的margin,但由于外边距叠加,它们的间距仍然是10px,而p3的容器指定了overflow属性(后面解释),创建了新的BFC,导致了p3的垂直外边距没有发生叠加,p2与p3的间距为20px。

  • BFC不会与浮动元素发生重叠
    更确切的说法是BFC的边框不会与其它浮动元素的外边距发生重叠,这意味着浏览器可能会给BFC创建隐式的外边距来实现该效果,参考下面的例子:

    我向左浮动(width:180px)
    我向右浮动(width:180px)
    div1 虽然文字没有被覆盖,但我与浮动元素重叠了

    我向左浮动(width:180px)
    我向右浮动(width:180px)
    div2 我创建了BFC,我与浮动元素没有发生重叠

    上面的例子中,浏览器为div2添加隐式的外边距来保证它的border-box不会与其它浮动的元素发生重叠,换句话说div2出场就自带了margin:0 180px;的特效,此时你为div2手动指定任何小于180px的水平外边距将得不到任何效果。此时如果你想在div2与浮动的元素之间添加10px的空隙,你需要为div2指定190px的水平外边距。
    最后,有人可能会想,这里的div2通过压缩自身的宽度从而保证了不与浮动元素发生重叠,假设我为div2指定一个较大的固定宽度,它将如何应对?答案是它会被挤到下面一行,依然不会发生重叠。

  • BFC会包含它里面浮动的元素
    对此w3c有明确的定义,参见'Auto' heights for block formatting context roots,其中核心的一句话是“如果BFC根元素包含浮动的后代元素,并且该后代元素的bottom margin edge低于元素的bottom content edge,则元素自动增加高度直到能够包含这些边缘。”原谅我蹩脚的翻译,还是直接来看例子吧:

    div1 我是容器
    我浮动了(margin:10px)

    div2 我是BFC容器
    我浮动了(margin:10px)

    上面例子中div2创建了新的块级格式化上下文,因此得以将浮动的子元素(包括其margin)包含其中。回顾一下文章的主题,这不正是我们所要的清除浮动效果吗?那么剩下的问题就是,我们应该如何让所有会包含浮动子元素的容器元素创建新的BFC。

2.如何创建BFC

下列任一情况将创建一个块格式化上下文(资料来源于MDN):

  • 根元素或其它包含它的元素
  • 浮动 (float:left|right)
  • 绝对定位元素 (position:absolute|fixed)
  • display属性(display:inline-block|table-cell|table-caption|flex|inline-flex)
  • overflow除visible外的任意值(overflow:hidden|auto|scroll)

3.如何通过BFC清除浮动

正如上一节看到的,创建BFC的方法有多少种,清除浮动的方法就有多少种。一开始有人尝试通过为容器指定float属性创建BFC,我只是想想都觉得不靠谱,清除浮动的方法居然是将所有元素都浮动起来。一种稍微靠谱的方法是将容器的overflow属性设为hidden或者auto,比起修改position与display,它的副作用(截断内容或滚动条)已经算是可以接受的了,但我们最开始的clear伪元素可是没有任何副作用的。好吧,这确实有点让人沮丧,尽管花了这么长的篇幅来说明BFC,但它还不如直接使用clear伪元素。幸运的是在clearfix最佳实践中,还会用到它的概念。

四.clearfix最佳实践与原理

回顾第二段,现在我们的clearfix版本是这样的:

1
2
3
4
5
.clearfix:after{
content:"";
display:block;
clear:both;
}

下面则是目前CSS清除浮动的最佳实践,来自Nicolas Gallagher,详细内容参见A new micro clearfix hack

1
2
3
4
5
6
7
8
9
10
.clearfix:before,.clearfix:after{
content:" "; /*注意里面的空格*/
display:table;
}
.clearfix:after{
clear:both;
}
.clearfix{
zoom:1; /* For IE 6/7 (trigger hasLayout) */
}

对比上面两个版本的不同,很容易提出下面几个问题。

1.content里面为什么需要一个空格?

根据原文的描述,在content添加空格是为了处理一个Opera浏览器的显示bug,由于这个问题我在自己的Opera上没有重现,因此也无法给出更详细的说明。

2.zoom:1有什么用?

由于伪元素与BFC是由CSS2.1规范提出,因此上面讨论的两种方式IE6与IE7均不支持,但它们有一个与BFC类似的概念叫hasLayout,限于篇幅,这里不再详细讨论,简单的说它同样具备BFC的作用即:

  • 决定外边距是否折叠
  • 不与浮动元素发生重叠
  • 包含它里面的浮动元素

它的触发条件是(满足任一即可):

  • float:left|right
  • position:absolute|fixed
  • display:inline-block
  • writing-mode:tb-rl
  • width:(除auto外任意值)
  • height:(除auto外任意值)
  • zoom:(除normal外任意值)

IE7还包括:

  • min-width|max-width:(任意值)
  • min-height|max-height:(任意值)
  • overflow:(除visible外任意值)
  • overflow-x|overflow-y:(除visible外任意值)

因此这里通过zoom:1触发IE6,IE7的hasLayout,其原理与创建BFC是一样的。

3.before伪元素有什么用?display属性为什么要设成table?

把它们放在一起是因为它们的目的都是为了阻止垂直外边距叠加。举个例子:

div1 我的垂直外边距与容器叠加(margin:20px)
div2 我的垂直外边距不与容器叠加(margin:20px)

它的原理用到了之前提到的BFC概念,display:table创建了匿名的display:table-cell框,而前面已经提到,display:table-cell将创建新的BFC,从而阻止了垂直外边距叠加。然而也许你已经意识到了,它跟清除浮动有一毛钱关系吗?是否需要垂直外边距叠加本应该由各自网站的开发人员来决定,换句话说为什么要在清除浮动的代码中强行加入设定垂直外边距是否叠加的代码?这就像通过设定overflow:hidden来清除浮动一样产生了副作用。
其实它是为了浏览器显示一致性而添加的代码,前面已经讨论过,假设我们通过创建BFC来清除浮动,那么它的垂直外边距不会叠加。同理,通过触发hasLayout也不会叠加。因此如果没有clearfix:beforedisplay:table,那么元素的垂直外边距在现代浏览器下会叠加,而在IE6与IE7中不会叠加,这就破坏了样式的一致性。换句话说,如果不需要支持IE6与IE7,那么使用如下代码就足够了:
1
2
3
4
5
.clearfix:after{
content:" "; /*注意里面的空格*/
display:block;
clear:both;
}

完!