BFC 块级格式化上下文、两列式布局与三列式布局

9 阅读10分钟

BFC 块级格式化上下文

BFC(Block Formatting Context) 是CSS 中独立的渲染隔离区域,区域内外的布局互不干扰、互不影响,所有的布局规则只在当前的BFC 内部生效。

BFC 是解决浮动高度塌陷、外边距(margin)折叠、实现两列、三列自适应布局的核心底层逻辑。

如何理解浮动高度塌陷?

  • 比如说:你有一个父容器(比如一个卡片),里面有一个子元素。子元素设置了 float: left。结果你发现,父容器的高度变成了 0,就像它没有内容一样,导致后面的元素跑到了它下面,布局全乱了。
  • 原因是:
    • 脱离文档流: 当一个元素设置了浮动(float),它就像“灵魂出窍”一样,脱离了正常的文档流
    • 父亲的错觉:  父容器在计算高度时,只会看“留在家里”(在文档流中)的孩子。因为浮动的孩子“飘”走了,父容器觉得家里没人,所以高度塌陷为 0。
  • 解决方法:给父容器添加 overflow: hidden 或 overflow: auto
    • 原理是BFC 有一个特性——计算高度时会包含内部的浮动元素。一旦父元素成为 BFC,它就会把“飘走”的孩子强行拉回来算高度。

如何理解外边距折叠?

  • 比如说你有两个垂直排列的div。上面的 div 设置 margin-bottom: 50px,下面的 div 设置 margin-top: 20px。直觉上它们中间应该有 70px 的间距。 现实是它们中间只有 50px 的间距。或者,你在一个父容器里放了一个子元素,给子元素设置margin-top: 20px,结果父容器也跟着往下跑了 20px。
  • 原因是:
    • 合并规则:  在垂直方向上,相邻的块级元素(兄弟或父子),它们的 Margin 会相遇。浏览器为了“省事”(或者说是为了排版美观),规定:当两个 Margin 相遇时,取较大的那个值,而不是相加。这就是 Margin 折叠。
    • 父子穿透:  如果父子元素之间没有“障碍物”(如边框、内边距),子元素的 Margin 会穿透父元素的边界,直接作用在父元素外部。
  • 解决方法:给父元素设置 overflow: hidden 或 display: flow-root(现代推荐)。
    • 原理是BFC 的另一个特性——BFC 区域内部的 Margin 不会与外部发生折叠。一旦父元素变成 BFC,它就变成了一个独立的结界,子元素的 Margin 被锁在里面了。

一、如何生成一个独立的BFC 区域

块级元素默认不是BFC,必须满足特定CSS 属性才会触发BFC,html 是页面天然的顶级BFC 容器。

1.根元素html

整个页面的根布局容器,也就是html是默认最大的BFC,页面内所有的普通块级元素都在这个BFC 里,遵循从上到下垂直布局的默认规则。

2.设置了浮动的元素

float:left/rightfloat:none 不触发)

浮动会让元素脱离标准文档流,同时强制创建BFC,缺点是脱离文档流副作用大,易引发父元素高度塌陷。

3.非static 的定位元素

position: absolute / fixed / sticky / relative

只要不是默认的 static 定位,都会创建 BFC;其中 absolute/fixed 完全脱离文档流,relative 仅偏移不脱离。

4.行内块元素

display: inline-block

兼具行内元素不换行、块级元素可设宽高的特性,同时触发 BFC。

5.表格相关

display 属性display: table-cell / table-caption

表格类元素天然具备 BFC 特性,日常布局中极少使用。

6.溢出非可见 作为BFC 用的最多

overflow: hidden / auto / scroll(overflow:visible 不触发)

日常开发最常用、无副作用的 BFC 触发方式,仅开启独立渲染规则,不改变元素文档流状态。

7.弹性布局或网格布局

display: flex / inline-flex / grid / inline-grid

现代布局方案,父容器设为 flex/grid 后,直接成为 BFC,内部布局完全独立。

二、BFC 内部布局规则(区域内元素的排布逻辑)

BFC 内部有一套固定的布局准则,不受外部样式干扰:

1.块级盒子垂直排列

BFC 内的所有块级元素,默认从上到下依次垂直排布,和标准文档流的块级布局一致,是最基础的排布规则。

2.垂直 margin 边距折叠

同一个 BFC 内部**,相邻块级盒子的垂直外边距会合并,最终间距取两个 margin 的最大值,而非相加。

例:上方盒子margin-bottom:20px,下方盒子margin-top:30px,最终两者间距为 30px。

拓展:父子元素的垂直 margin、空块级元素的 margin,在同一 BFC 内也会折叠。

讲解:空块级元素的 Margin 折叠(自我吞噬)

  • 现象:你写了一个空的 div(或者里面只有一个 <br>),给它设置了 margin-top: 20px 和 margin-bottom: 30px。你期望它占据 50px 的高度。结果它高度变成了 0,或者只占据了 30px(取最大值),仿佛这个元素消失了一样。

  • 原理(如何理解):

    • 首尾相接:  对于一个高度为 0、没有边框、没有内边距的空盒子,它的“顶部”和“底部”在同一个位置。
    • 内外合并:  这个盒子的 margin-top(想推开上面的元素)和 margin-bottom(想推开下面的元素)相遇了。因为它们都属于同一个 BFC 里的垂直间距,浏览器把它们合并了。

3.块级盒子触左边缘对齐

BFC 内的块级盒子,默认紧贴 BFC 容器左边缘排布,不会主动避让 BFC 内部的浮动元素

这也是关键现象:浮动图片旁的文字 / 行内元素会环绕浮动,但块级盒子会直接钻到浮动元素下方,因为块级盒只遵循 BFC 边界规则,不识别浮动。

4.布局独立计算

BFC 是完全独立的渲染单元,内部元素的尺寸、位置计算只参考自身 BFC 规则,外部的浮动、margin 等样式不会影响内部布局。

三、BFC 与外部交互规则(核心实用价值,布局关键)

这是 BFC 支撑两列、三列布局、解决布局 bug 的核心规则,也是日常开发最常用的能力:

规则 1:BFC 会完整包裹内部浮动元素 → 解决「父元素高度塌陷」

高度塌陷原理

普通父元素(非 BFC)不会计算脱离文档流的浮动子元素高度,子元素浮动后,父元素无法被撑开,高度直接坍缩为 0,这就是高度塌陷。

BFC 的解决逻辑

BFC 的渲染规则会将内部浮动元素纳入高度计算范围,父元素触发 BFC 后,会主动包裹所有浮动子元素,高度被正常撑起,无需再用 clearfix 清除浮动。

规则 2:BFC 区域不会与外部浮动元素重叠 → 实现自适应两列 / 三列布局

核心原理

普通块级元素会无视外部浮动,直接钻到浮动元素下方;而 BFC 会主动识别外部浮动元素,自动收缩自身宽度,避让浮动区域,不发生重叠。这也是 BFC 支撑多列布局的核心逻辑。

四、利用BFC 实现两列式布局

方法一:float + overflow 可以实现两列式布局

使用html 语义化标签aside 侧边栏和main 主栏作为两列式布局的载体。

给aside 设置float:left,使aside 脱离当前文档流称为一个浮动元素,同时向父容器左边靠。

main 是普通块级元素(默认占满整个宽度),它会无视浮动元素,自动移动到父容器的顶部占满整个宽度并被aside 覆盖重叠(aside 已经是浮动元素,脱离文档流了)左边的200px。

给 main 设置 overflow: hidden 使其形成 BFC。

此时main 是BFC,根据BFC 的特性,BFC 不会与浮动元素重叠,因此 main 会自动紧贴 aside 右侧排列,实现两栏布局。

  • 当 main 触发 BFC 后,它计算宽度的公式变成了:
    • main 的宽度=父容器宽度−aside 的宽度main 的宽度=父容器宽度−aside 的宽度

代码展示:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        .layout {
            color: #fff;
            padding: 0 100px;
            min-width: 500px;
        }

        h4 {
            position: relative;
            color: #333;
            margin: 20px 0 10px;
        }

        h4::before {
            content: '#';
            position: absolute;
            left: -20px;
            top: -1px;
            font-size: 18px;
            color: #38ab89;
        }

        section {
            margin-bottom: 50px;
        }

        aside {
            width: 200px;
            height: 50px;
        }

        .orange {
            background-color: orange;
        }

        .green {
            background-color: green;
        }

        .float {
            /* 关键代码 */
            float: left;
        }

        .hidden {
            /* 关键代码 */
            overflow: hidden;
        }    
    </style>
</head>

<body>
    <div class="layout">
        <h4>方法一:float + overflow</h4>
        <section>
            <aside class="orange float">aside</aside>
            <main class="green hidden">main</main>
        </section>
    </div>
</body>

</html>

方法二:float + margin-left 可以实现两列式布局

给 aside 设置 float: left,使其脱离文档流并向父容器左侧靠拢。

后面的 main 是普通块级元素(默认占满整个宽度),它会无视浮动元素,自动移动到父容器的顶部并尝试占满整个宽度,导致其左侧部分被 aside 覆盖重叠。

给 main 设置 margin-left: 200px(值等于 aside 的宽度)。

利用外边距产生的占位空间,把 main 的内容区域整体“挤”到 aside 的右侧,从而避开了浮动区域。

main 不再与浮动元素发生重叠,因此会自动紧贴 aside 右侧排列,实现两栏布局。

这个方法并没有用到BFC 特性,但也是实现两列式布局的好方法。

代码展示:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        .layout {
            color: #fff;
            padding: 0 100px;
            min-width: 500px;
        }

        h4 {
            position: relative;
            color: #333;
            margin: 20px 0 10px;
        }

        h4::before {
            content: '#';
            position: absolute;
            left: -20px;
            top: -1px;
            font-size: 18px;
            color: #38ab89;
        }

        section {
            margin-bottom: 50px;
        }

        aside {
            width: 200px;
            height: 50px;
        }

        .orange {
            background-color: orange;
        }

        .green {
            background-color: green;
        }

        /* 浮动:左边固定宽度 */
        .float {
            float: left;
        }

        /* 关键:margin 代替 overflow */
        .mian {
            margin-left: 200px;
            /* 👈 左边宽度是200px,这里就给200px外边距 */
        }
    </style>
</head>

<body>
    <div class="layout">
        <h4>方法二:float + margin</h4>
        <section>
            <aside class="orange float">aside</aside>
            <main class="green main">main</main>
        </section>
    </div>
</body>

</html>

五、三列式布局如何实现

圣杯布局方式

给父容器设置左右 padding,预留出两侧的空白区域;

中间栏 main 不设置宽度,作为普通块级元素自动占满父容器的内容区域;

左右栏使用 position: relative 进行相对定位,配合负的 left / right 偏移值,将侧边栏“拉”进父容器的 padding 留白区域;

中间栏保持在正常文档流中排列,不受浮动影响;左右栏则通过定位偏移实现占位;

最终实现两侧固定宽度、中间自适应的经典三栏布局。

代码展示:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        .layout {
            color: #fff;
            padding: 0 100px;
            min-width: 500px;
        }

        .w150 {
            width: 150px;
            height: 250px;
        }

        main {
            height: 300px;
        }

        .orange {
            background-color: orange;
        }

        .green {
            background-color: green;
        }

        .grail {
            padding: 0 150px;
            /* 父容器留白 */
            overflow: hidden; /* 触发 BFC,包含浮动,避免父元素高度塌陷 */
        }

        .grail main {
            width: 100%;
            float: left;
        }

        .grail .left {
            position: relative;
            margin-left: -100%;
            float: left;
            left: -150px;
        }

        .grail .right {
            float: left;
            position: relative;
            margin-left: -150px;
            right: -150px;
        }
    </style>
</head>

<body>
    <div class="layout">
        <h4>方法一:圣杯布局</h4>
        <section class="grail clearfix">
            <main class="green">main</main>
            <aside class="w150 left orange">aside</aside>
            <aside class="w150 right orange">aside</aside>
            <!-- 父容器所有元素都浮动,会造成父容器高度塌陷,导致内容被遮挡
            解决方法:添加伪元素清除浮动
            .clearfix::after {
            content: "";
            display: table;
            clear: both;
            } -->
        </section>
    </div>
</body>

</html>