toFrankie / blog

种一棵树,最好的时间是十年前。其次,是现在。
20 stars 1 forks source link

你不知道的 Margin #311

Open toFrankie opened 1 year ago

toFrankie commented 1 year ago

配图源自 Freepik

只要你是 Web 前端开发,是在写页面,几乎离不开 Margin 吧。

那么,你真的了解它吗?有哪些特别的应用?

本文就以下几个方面展开介绍:

从大家知道的讲起

语法

Margin 用于设置元素的外边距,其语法很简单。

div {
  margin: 10px; /* 一个属性值 */
  margin: 10px 20px; /* 两个属性值 */
  margin: 10px 20px 30px; /* 三个属性值 */
  margin: 10px 20px 30px 40px; /* 四个属性值 */
}

四种方式对应的含义大家都懂,就不再啰嗦了,略过...

属性值

其属性值可接受具体值(length)、百分比值(percentage)和 auto 。顺便提一句,百分比值通常根据父元素宽度来确定大小,更严谨应该称为包含块(Containing Block)。

作用范围

除了行内元素设置 margin-topmargin-bottom 无效,包括 inline-block 在内的所有元素都会起作用。

逻辑属性

我们知道 margin-left 等方向是一成不变的,永远表示左外边距。但是与 Margin 相关的逻辑属性 margin-inlinemargin-block,它们实际排版方向会随着书写模式等改变而改变,也就是 margin-inline-start 等可能是左外边距,也可能是右外边距。

默认情况下,margin-inline 对应水平方向,margin-block 对应垂直方向。这个跟我们平常的阅读或书写习惯是相同的。

margin-blockmargin-block-startmargin-block-end 的简写。 margin-inlinemargin-inline-startmargin-inline-end 的简写。

语法如下:

div {
  margin-inline: 10px; /* 一个属性值,应用于行首和行末 */
  margin-inline: 10px 20px; /* 一个属性值,第一个应用于行首,第二个应用于行末 */
}

margin-block 同理。通常我们的书写习惯是从左至右、从上至下,此时 margin-inline 对应 margin-leftmargin-rightmargin-block 对应 margin-topmargin-right

*-start*-end*-inline-start*-inline-end 等这类属性是 CSS 逻辑属性,它们会根据 writing-modedirectiontext-orientation 所定义的值去对应 margin-top 等属性。此处不展开介绍,有兴趣自行查阅。

Margin Auto

当设置为 auto 时,浏览器会自动计算外边距。用得最多的就是 margin: automargin: 0 auto,可使得块级元素水平居中对齐。

以上两种写法是 margin: auto auto automargin: 0 auto 0 auto 的简写。

假设要实现以下这种布局:子元素宽度是父元素的 30%,且居右对齐。

那么我们只要给子元素设置一个 margin-left: auto 就可居右对齐。

<div style="border: 1px solid red">
  <div style="width: 30%; height: 100px; margin-right: auto; background: green"></div>
</div>

一般情况下,给上下外边距设置 auto 无效的原因是浏览器自动计算结果为 0,此时 marign: auto 相当于 margin: 0 auto。对于左右外边距而言,如果一侧定宽,一侧 auto,auto 则为剩余空间大小。如果两侧均为 auto,则两侧平分剩余空间。

其实 margin: auto 在特定条件下可以使得元素在水平和垂直方向实现居中,那就是绝对定位的元素。

<div id="parent">
  <div id="child"></div>
</div>
#parent {
  position: relative;
  margin: 0 auto;
  border: 1px solid red;
  width: 200px;
  height: 200px;
}

#child {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  margin: auto;
  width: 100px;
  height: 100px;
  background: green;
}

实现居中对齐的关键点:一是上下左右偏移值均为 0,二是 margin: auto

Margin 与 Relative Position 的区别

某些情况下,使用 margin 或 position 都能实现相同的排版。比如:

但是,如果我们向 item 元素中追加一个子元素,二者区别就能窥探一二了。

尽管前面示例,在效果上都是“向下偏移”了 30 个像素,但是 margin 会影响文档流的位置,除了其本身位置发生偏移,其后的处于标准文档流的元素都跟着发生变化。而 position: relative 则不同,它自对自身有影响,它本身在文档流中的位置并无发生变化,因此其后元素仍处在原有位置,因此截图中看到两个元素在表现上重叠在一起了。

CodeSandbox Demo

外边距折叠

我们知道,假设有如下两个相邻的普通(块级)元素,两个元素之间的外边距会发生重叠,取最大值 50px 作为两者之间的距离,这种现象称为「外边距折叠(margin collapsing)」。

<div style="margin-bottom: 30px">Hey,</div>
<div style="margin-top: 50px">Frankie</div>

为什么要折叠?

CSS1 Vertical formatting

The width of the margin on non-floating block-level elements specifies the minimum distance to the edges of surrounding boxes. Two or more adjoining vertical margins (i.e., with no border, padding or content between them) are collapsed to use the maximum of the margin values. In most cases, after collapsing the vertical margins the result is visually more pleasing and closer to what the designer expects.

CSS2 Collapsing margins

In CSS, the adjoining margins of two or more boxes (which might or might not be siblings) can combine to form a single margin. Margins that combine this way are said to collapse, and the resulting combined margin is called a collapsed margin.

从规范描述可知,这种折叠行为是故意为之,目的是使得内容排版在视觉上更美观。另外,有兴趣可以翻翻这文章的回答,历史原因似乎跟 <p> 元素排版有关,然后一直延续下来。

什么时候发生折叠?

折叠只发生块级元素的垂直方向上,水平方向是永远不会的。

<div style="margin-bottom: 30px; display: inline-block">Hey,</div>
<div style="margin-top: 50px">Frankie</div>

上述示例,由于第一个元素设为 inline-block,因此两个元素之间未产生折叠现象,两者在垂直方向的间距为 80px。下文若无特别说明,在介绍折叠的时候默认元素为块级元素。

我们知道,标准文档流中元素会从左到右、从上往下的流式排列。如果一个元素脱离了文档流,该元素会从默认排列中移去,不再占据空间,其后的元素会往上或往左移动。脱离文档流的方式有 float、absolute position 和 fixed position。

当两个元素满足以下条件时会发生外边距折叠:

  1. 处于同一块级格式上下文(BFC)中的两个块级元素,而且元素均属于标准文档流。
  2. 两个元素之间没有被非空元素(包括有内容,但高度为 0 的情况)、清除浮动、边框、内边距隔开。
  3. 两个元素在垂直方向上是「相邻」的

满足以下情况则视为相邻(毗邻):

  • 元素的 margin-top 和它的第一个标准文档流子元素的 margin-top
  • 元素的 margin-bottom 和它的下一个标准文档流元素的 margin-top
  • 如果父级自动计算高度,元素是最后一个且为标准文档流,元素的 margin-bottom 和父级元素的 margin-bottom
  • 元素(本身)的 margin-topmargin-bottom,且未创建新的块级格式上下文的标准文档流元素,其高度表现为零。包括 height: 0min-height: 0height: auto 且标准文档流的无子元素三种情况。

注意点:

  • 元素相邻不一定是兄弟或祖先元素。
  • 浮动元素与其他任意元素不会发生折叠。
  • 当一个元素创建了新的块级格式上下文后,它不会与其子元素发生折叠。
  • 绝对定位的元素不会发生折叠,无论是其兄弟元素,还是其子元素。
  • 行内元素不会发生折叠。
  • 当发生折叠时,产生的边距取值分为几种情况:
    • 外边距均为正数时,取最大值。
    • 外边距均为负数时,取绝对值最大的值。
    • 外边距为一正一负时,取两者相加的和。

折叠示例

接下来,将列举部分示例说明。

示例一

<div style="height: 20px; background: red"></div>
<div style="height: 40px; background: green; margin-top: 20px">
  <div style="height: 20px; background: blue; margin-top: 50px"></div>
</div>

绿色块(父)与蓝色块(子) margin-top 发生了折叠,且取最大值,因此与红色块的边距是 50px。

示例二

<div style="height: 20px; background: red"></div>
<div style="background: green; margin-bottom: 20px">
  <div style="height: 20px; background: blue; margin-bottom: 50px"></div>
</div>
<div style="height: 20px; background: yellow"></div>

绿色块元素为自动高度,此时绿色块(父)与蓝色块(子) margin-bottom 发生了折叠,且取最大值,因此与黄色块的边距是 50px。

示例三

<div style="height: 20px; background: red"></div>
<div style="background: green; margin-top: 20px; margin-bottom: 20px"></div>
<div style="height: 20px; background: blue"></div>

中间绿色自动高度,且无子元素,相当于 height0。此时绿色块本身发生折叠,因此红色块与蓝色块之间的边距为 20px。

但如果我们给零高的元素加些内容,它就不会折叠了。比如:

<div style="height: 20px; background: red"></div>
<div style="background: green; margin-top: 20px; margin-bottom: 20px; height: 0">some text...</div>
<div style="height: 20px; background: blue"></div>

示例四

<div style="height: 20px; background: red"></div>
<div style="background: green; margin-bottom: 20px">
  <div style="height: 20px; position: absolute"></div>
  <div style="height: 20px; background: blue; margin-top: 50px"></div>
</div>

可以看到红色块与蓝色块的边距为 50px,但蓝色块并非是红色块的第一个子元素,但它是红色块的第一个标准文档流的子元素,因此这种情况也是满足相邻条件的。

示例五

<div style="height: 20px; background: red; margin-bottom: 20px"></div>
<div></div>
<div style="height: 20px; background: green; margin-top: 50px"></div>

image.png

中间隔了个零高元素,且无子元素,也会发生折叠,边距为 50px。

我们将中间的元素加一些内容,并显式指定 height0

<div style="height: 20px; background: red; margin-bottom: 20px"></div>
<div style="height: 0">some text...</div>
<div style="height: 20px; background: green; margin-top: 50px"></div>

尽管中间元素仍为零高,可由于其子元素有内容,因此红色块与绿色块并未发生折叠,边距为 70px。

如何清除外边距折叠?

根据前面发生折叠的条件可知,只要破坏任一条件,使其不满足就能清除折叠现象,通常采用创建新的块级格式上下文的方式。

创建新 BFC 的几种方式:

由于 BFC 内的子元素无论如何排列,都不会影响外部的元素,因此也可以用于避免外边框折叠。

负值 Margin

平常使用 Margin 都是正值居多,那么负值 Margin 会发生什么有趣的事情呢?

简单总结:

属性 设置负值的行为
margin-top 元素本身向上偏移
margin-left 元素本身向左偏移
margin-right 元素本身不偏移,其右边元素向左偏移
margin-bottom 元素本身不偏移,其下方元素向上偏移

一些奇奇怪怪的表现:

  • 若块级元素父级为自动高度,子元素中设置负值 margin-top,会「改变」元素父级在标准文档流中占用的高度。
  • 若块级元素为自动宽度,元素设置负值 margin-left 或 margin-right,会使得元素向左侧或右侧增加宽度。

负值的外边距折叠

当发生折叠时,如果都是正值的话,取最大值作为边距。但如果外边距为一正一负或者均为负值呢?

一正一负

<div id="parent">
  <div id="child1" style="height: 20px; background: red; margin-bottom: 20px"></div>
  <div id="child2" style="height: 20px; background: green; margin-top: -30px"></div>
</div>

上述示例,child1 和 child2 发生折叠,元素间的边距为两数之和 -10px,因此 child2 向上偏移 10px,覆盖了 child1 的下半部分。请注意,文档流只会向上或向左流动,不能向下或向右流动的

均为负数

<div id="parent">
  <div id="child1" style="height: 20px; background: red; margin-bottom: -20px"></div>
  <div id="child2" style="height: 20px; background: green; margin-top: -30px"></div>
</div>

上述示例,child1 和 child2 发生折叠,元素间的边距取绝对值最大者,即为 -30px。所以 child2 向上偏移 30px,child2 覆盖了 child1 的上半部分。

负值 margin-top、margin-bottom

无论正值,还是负值,垂直方向的外边距只对块级元素产生作用。同样地,无论正负值都会影响元素在标准文档流的位置或空间,只不过正值是直觉性的,理解起来很自然。而负值似乎有点反直觉罢了。

属性 设置负值的行为
margin-top 元素本身向上偏移
margin-bottom 元素本身不偏移,其下方元素向上偏移

另外,如果元素父级是自动计算的高度,在子元素中设置负值的 margin-top 或 margin-bottom 的话,最终会影响父级在标准文档流中的高度。

负值 margin-top

<div id="parent">
  <div id="child1" style="height: 60px; background: red"></div>
  <div id="child2" style="height: 20px; background: green; margin-top: -60px"></div>
  <div id="child3" style="height: 20px; background: blue"></div>
</div>

上述示例,child2 设置了 margin-top: -60px,元素自身(绿色块)向上偏移了 60px,所以跟 child1 的上方重合。当 child2 在标准文档流中的位置向上偏移后,其后的 child3 元素也跟着向上流动,紧跟在 child2 之后。

如果 child2 未设置 margin-top: -60px 的话,parent 元素的高度应为 height(child1 + child2 + child3) = 100px。可设置负值 margin-top 后,使得从 child2 元素起标准文档流的位置向上移动了 60px,所以 parent 元素的高度为 height(child1 + child2 + child3) + 垂直方向的偏移值,即 60 + 20 + 20 - 60 = 40px

负值 margin-bottom

<div id="parent">
  <div id="child1" style="height: 40px; background: red"></div>
  <div id="child2" style="height: 20px; background: green; margin-bottom: -60px"></div>
  <div id="child3" style="height: 20px; background: blue"></div>
</div>

以上示例,child2 设置了 margin-bottom: -60px,元素本身(绿色块)未发生偏移,仍在紧跟在 child1 之后。但 child2 的负值 margin-bottom 会使得其下方的 child3 元素向上偏移 60px,所以跟 child1 的上方重合。看起来像换了位置一样。

如果 child2 未设置 margin-top: -60px 的话,parent 元素的高度应为 height(child1 + child2 + child3) = 80px。虽然负值的 margin-bottom 不会使自身在标准文档流的位置向上移动,但它会使其下方的元素在标准文档流的位置也会发生变化,且向上移动,有点像给下方元素设置了负值 margin-top 的意思,所以 parent 元素的高度为 height(child1 + child2 + child3) + 垂直方向的偏移值,即 40 + 20 + 20 - 60 = 20px

小结

负值的 margin-top 和 margin-bottom 都会使得元素在标准文档流中的位置,区别在于影响本身还是其后的元素,进而可能会影响到父级元素在文档流中占用的空间(指高度)。当然,如果父级元素指定了具体高度,将不会其高度将不会受到影响。

尽管前面设置了负值边距,但是还是可以看到他们被完整地绘制出来了,原因是元素的 overflow 属性默认溢出可见,如果前面的示例中 parent 元素指定 overflow: hidden 的话,你将会看到只有 40px 和 20px 的大小。还有,子元素占用的空间大小是不会因为设置了 margin-top/margin-bottom 值发生变化的。

可以简单地这样理解,在文档流的眼里,元素的在文档流中的开始位置是由 margin 决定的,为正数则增加,为负数则减小,而不是看元素实际大小的。

负值 margin-left 和 margin-right

我们知道,对于自动宽度的块级元素,设置正值的 margin-left 或 margin-right 会改变减小元素宽度。相反地,负值会增加宽度。

属性 设置负值的行为
margin-left 元素本身向左偏移
margin-right 元素本身不偏移,其右边元素向左偏移

增加宽度

我们给一个自动计算宽度的块级元素设置左右外边距设为 -20px,它的宽度增长了 40px。

<div style="border: 1px solid red; margin-left: -20px; margin-right: -20px">关关雎鸠,在河之洲。窈窕淑女,君子好逑。参差荇菜,左右流之。窈窕淑女,寤寐求之。求之不得,寤寐思服。悠哉悠哉,辗转反侧。参差荇菜,左右采之。窈窕淑女,琴瑟友之。参差荇菜,左右芼之。窈窕淑女,钟鼓乐之。</div>
<div style="height: 20px; background-color: green; margin-top: 20px"></div>

负值 margin-left

我们在这段文字中插入了一个 inline 元素和一个 inline-block 元素,未设置 margin 的表现如下:

<div style="border: 1px solid red">关关雎鸠,在河之洲。窈窕淑女,君子好逑。参差荇菜,左右流之。窈窕淑女,寤寐求之。 <span style="background-color: darkorange; margin-left: 0px">我是 inline 元素。</span>求之不得,寤寐思服。悠哉悠哉,辗转反侧。<span style="display: inline-block; background-color: plum; margin-left: 0px">我是 inline-block 元素。</span>参差荇菜,左右采之。窈窕淑女,琴瑟友之。参差荇菜,左右芼之。窈窕淑女,钟鼓乐之。</div>

接着,给 inline 和 inline-block 元素加上 margin-left: -50px

<div style="border: 1px solid red">关关雎鸠,在河之洲。窈窕淑女,君子好逑。参差荇菜,左右流之。窈窕淑女,寤寐求之。<span style="background-color: darkorange; margin-left: -50px">我是 inline 元素。</span>求之不得,寤寐思服。悠哉悠哉,辗转反侧。<span style="display: inline-block; background-color: plum; margin-left: -50px">我是 inline-block 元素。</span>参差荇菜,左右采之。窈窕淑女,琴瑟友之。参差荇菜,左右芼之。窈窕淑女,钟鼓乐之。</div>

可以看到除了 inline 和 inline-block 元素向左偏移之外,其后的内容也是跟着移动的。说明它们的在标准文档流的位置发生了变化。

负值 margin-right

还是在前面的示例基础上,给 inline 和 inline-block 元素加上 margin-right: -50px

<div style="border: 1px solid red">关关雎鸠,在河之洲。窈窕淑女,君子好逑。参差荇菜,左右流之。窈窕淑女,寤寐求之。<span style="background-color: darkorange; margin-right: -50px">我是 inline 元素。</span>求之不得,寤寐思服。悠哉悠哉,辗转反侧。<span style="display: inline-block; background-color: plum; margin-right: -50px">我是 inline-block 元素。</span>参差荇菜,左右采之。窈窕淑女,琴瑟友之。参差荇菜,左右芼之。窈窕淑女,钟鼓乐之。</div>

请仔细观察,inline 元素(本身)并未发生移动,但其右边的元素则向左偏移,导致覆盖在元素上了,inline-block 同理。对比前面负值 margin-left 更明显。

小结

对于 margin-left 和 margin-right 无论正负值都比较容易理解的。

负值 Margin 的应用

自适应三栏布局

相信大家都听说过「圣杯布局」、「双飞翼布局」这些经典的三栏布局,本质上就是利用了浮动和负值 Margin 实现的。当然,这些经典布局方式有更现代化的解决方案,像 Flex、Grid 等,再利用 Media Queries 可以实现适配移动端、PC 端的响应式布局。

圣杯布局是由 Matthew Levine 于 2006 年提出的,后来国内提出了改进版的双飞翼布局,据说是玉伯大佬提出的,我未找到出处或原贴。下面将会介绍两者的区别,以及改进了什么问题。

圣杯布局(Holy Grail Layout)

出处:In Search of the Holy Grail

圣杯布局长这样 👇,上下分别为 header、footer,中间是三列布局,有 left、middle、right,其中左右是定宽的,中间则根据窗口大小自适应,而且 container 的高度也是根据内容自适应调整的。

DOM 结构如下:

<div id="header"></div>
<div id="container">
  <div id="middle"></div>
  <div id="left"></div>
  <div id="right"></div>
</div>
<div id="footer"></div>

说明:

实现:

<div id="container">
  <div id="middle" class="column"></div>
  <div id="left" class="column"></div>
  <div id="right" class="column"></div>
</div>
<div id="footer"></div>
#header {
  height: 50px;
  background: #eee;
}

#container {
  padding-right: 100px; /* left 宽度大小 */
  padding-left: 100px; /* right 宽度大小 */
}

#middle,
#left,
#right {
  float: left;
}

#middle {
  width: 100%;
  background-color: green;
}

#left {
  position: relative;
  left: -100px; /* 使其位置偏移至 container 的 padding-left 区域 */
  margin-left: -100%; /* 本身及其后元素均向左偏移 100%(相对于父元素宽度,即 container 的宽度) */
  width: 100px;
  background-color: red;
}

#right {
  position: relative;
  right: -100px; /* 使其位置偏移至 container 的 padding-right 区域 */
  margin-left: -100px; /* 使其再向左偏移 100px,该指需与 container 的 padding-right 和本身大小一致 */
  width: 100px;
  background-color: blue;
}

#footer {
  clear: both; /* 清除浮动 */
  height: 50px;
  background: #eee;
}

.column {
  height: 300px;
}

圣杯布局有一缺点是当浏览器窗口过小,布局就完全变形了,比如:

那么当窗口缩小到多少会发生变形呢?

我们来分析一下原因,前面 middle、left、right 的宽度分别为 100%(即父级元素的宽度)、100px100px。当窗口缩小至 width(middle) < width(left) 时,就开始变形。假设 middle 的宽度为 47px,那么 left 元素的 margin-left: -100% 向左偏移的值为 47px,此时 left 元素的文档流(脱离文档流)左侧起始位置与 middle 左侧重合,但由于 middle 的宽度为 47px,而 left 的宽度为 100px,也就是 middle 这行容不下 left 元素,因此 left 就“移”到下一行去了。同时受到 margin-left: -47pxposotion: relative; left: -100px 的影响,元素先向左偏移了 47px,然后再基于当前位置再向左偏移 100px,于是仅剩下上图红色可见部分。

因此,Matthew Levine 在 In Search of the Holy Grail 一文中通过控制 body 的最小宽度处理该问题。

body {
  min-width: 550px;  /* 2x LC width + RC width */
}

双飞翼布局

玉伯大佬提出的双飞翼布局,两者有什么区别呢?

先对比下 DOM 结构:

<!-- 圣杯布局 -->
<div id="header"></div>
<div id="container">
  <div id="middle"></div>
  <div id="left"></div>
  <div id="right"></div>
</div>
<div id="footer"></div>
<!-- 双飞翼布局 -->
<div id="header"></div>
<div id="middle">
  <div id="content"></div>
</div>
<div id="left"></div>
<div id="right"></div>
<div id="footer"></div>

区别在于双飞翼布局,在 middle 内增加了一个元素 content,其中 content 设置左右外边距以避免内容被 left 和 right 覆盖住。另外由于不用再像圣杯布局那样在 container 中设置左右内边距,因此可以把该元素干掉。

实现:

<div id="header"></div>
<div id="middle">
  <div id="content" class="column"></div>
</div>
<div id="left" class="column"></div>
<div id="right" class="column"></div>
<div id="footer"></div>
#header {
  height: 50px;
  background: #eee;
}

#middle,
#left,
#right {
  float: left;
}

#middle {
  width: 100%;
}

#content {
  margin-right: 100px;
  margin-left: 100px;
  background-color: green;
}

#left {
  margin-left: -100%;
  width: 100px;
  background-color: red;
}

#right {
  margin-left: -100px;
  width: 100px;
  background-color: blue;
}

#footer {
  clear: both;
  height: 50px;
  background: #eee;
}

.column {
  height: 300px;
}

但其实它也并没有解决圣杯模式窗口缩小的问题,当 width(middle) < width(left) 时仍会变形,left 部分会掉下来。

小结

随着 CSS 越来越强大,实现上述三列布局有更多、更好的现代化解决方案,大家可以尝试使用 Flex、Grid 等方式实现,此文就不再展开了。

另外,前面两种布局方式,都将 middle 部分放在前面,也就是说在 DOM 渲染时优先渲染主要内容部分,所以它会导致 DOM 顺序与视觉顺序不一致,进而影响到可访问性(Accessibility,A11Y),又称无障碍。当视障人群使用屏幕阅读器等工具访问网页时,由于顺序的不一致,它们可能会感到困惑。

相关文章:Source order and display order should match

多列等高布局

同样地,实现这种布局有 Flex、Grid 等现代化的解决方案。这里介绍一种利用了 float、margin 和 padding 实现「视觉等高」的方式。

以三列等高为例,其 DOM 结构如下:

<div id="container">
  <div id="left"></div>
  <div id="middle"></div>
  <div id="right"></div>
</div>

这里我们给 left、middle、right 三个元素设置背景色以便于判断是否等高。还有需将 left、middle、right 设为 float: left,每个元素设置同等大小的正直 padding-bottom 和负值 margin-bottom,并且这个值要足够大。我们知道负值 margin-bottom 会使得其后元素的文档流位置向上偏移,若它本身是最后一个元素,就相当于自身向上偏移。在 padding-bottommargin-bottom 的同时作用下,使得文档流最后的位置与该元素高度底部对应的重合,这样也就能按内容自适应高度了。而且由于背景色区域也包括 padding 部分,因此视觉上看着就等高,实则不是。最后,要记得往 container 部分加上 overflow: hidden,否则渲染出来的高度为三者中 height + padding-bottom 最大的那个。

#container {
  overflow: hidden;
  margin: 0 auto;
  width: 100%;
}

#left {
  float: left;
  margin-bottom: -1000px; /* 示例里就不设过大的值了 */
  padding-bottom: 1000px;
  width: 33.33%;
  height: 100px;
  background-color: red;
}

#middle {
  float: left;
  margin-bottom: -1000px;
  padding-bottom: 1000px;
  width: 33.33%;
  height: 200px;
  background-color: green;
}

#right {
  float: left;
  margin-bottom: -1000px;
  padding-bottom: 1000px;
  width: 33.34%;
  height: 300px;
  background-color: blue;
}

#footer {
  height: 100px;
  background: #eee;
}

现在 container 整体的高度取决于三者最大的那个,也就是 right 的 300px。

但如果我们将 right 的高度设为 1300px 的话,这种布局的问题就暴漏出来了,它们不等高了,原因很简单 left 和 right 的背景色高度是 height + padding-bottom,也就是 1100px 和 1200px,所以就不等高了(如下图所示)。当然,这个问题也很好解决,实际应用中把 padding-bottom 设得足够大即可。本文是为了举例故而设得较小。

去除边框

还是利用负值 margin-bottom 呗。

示例一

<ul>
  <li>关关雎鸠,在河之洲。窈窕淑女,君子好逑。</li>
  <li>参差荇菜,左右流之。窈窕淑女,寤寐求之。</li>
  <li>求之不得,寤寐思服。悠哉悠哉,辗转反侧。</li>
  <li>参差荇菜,左右采之。窈窕淑女,琴瑟友之。</li>
  <li>参差荇菜,左右芼之。窈窕淑女,钟鼓乐之。</li>
</ul>
li {
  border-bottom: 2px solid red;
}

假设我们要干掉最后一个 <li> 的下边框,首先想到的可能是 li:last-child { border-bottom: none } 等常规解法。然后我们今天充分了解 Margin 的特性后,我们可以使用 margin-bottom 来解决,比如:

#box {
  overflow: hidden;
}

ul {
  margin-bottom: -2px;
}

li {
  border-bottom: 2px solid red;
}
<div id="box">
  <ul>
    <li>关关雎鸠,在河之洲。窈窕淑女,君子好逑。</li>
    <li>参差荇菜,左右流之。窈窕淑女,寤寐求之。</li>
    <li>求之不得,寤寐思服。悠哉悠哉,辗转反侧。</li>
    <li>参差荇菜,左右采之。窈窕淑女,琴瑟友之。</li>
    <li>参差荇菜,左右芼之。窈窕淑女,钟鼓乐之。</li>
  </ul>
</div>

上面 margin-bottom 使得文档流向上偏移 2px,也就是说 <ul> 的高度减少了 2px,所以我们在外层设置一个 overflow: hidden 就能隐藏最底下的边框了。

示例二

<ul>
  <li>关关雎鸠,在河之洲。窈窕淑女,君子好逑。</li>
  <li>参差荇菜,左右流之。窈窕淑女,寤寐求之。</li>
  <li>求之不得,寤寐思服。悠哉悠哉,辗转反侧。</li>
  <li>参差荇菜,左右采之。窈窕淑女,琴瑟友之。</li>
  <li>参差荇菜,左右芼之。窈窕淑女,钟鼓乐之。</li>
</ul>
ul {
  border: 2px solid red;
}

li {
  margin-bottom: -2px;
}

假设我们要实现一个类似于单行簿的样式,其中 <ul> 外部设置了 2px 的边框,它与最后一个 <li> 下边框重复了,因此我们可以给 <li> 设置一个 margin-bottom: -2px 即可。但注意,这种方式会导致每个元素都会减少 2px,所以这里设置后会总体减少 10px。

li {
  margin-bottom: -2px;
  border-bottom: 2px solid red;
}

宫格布局

要实现这样的布局,要怎么做呢?为了介绍 Margin 的应用,下面我们不用 Flex 等布局方式哈。

假设每个格子宽高为 100px,格子间距为 10px,这样的话容器的宽度应该为 540px。我们脑海中第一反应可能是使用 nth-child() 等方式设置右侧的 margin-right 为 0,但如果是 3 列或 4 列呢,计算还有点烦,那么我们是不是可以利用负值 margin-right 可以增加元素宽度的特性去解决呢?

其 DOM 结构如下:

<div id="container">
  <div id="box">
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
  </div>
</div>
#container {
  margin: 0 auto;
  border: 1px solid red; /* 边框 */
  width: 540px; /* 容器宽度 */
}

#box::after {
  display: block;
  clear: both; /* 清除浮动,使得 container 高度不坍塌 */
  content: '';
}

.item {
  float: left; /* 设为左浮动,使得每个 item 向左排列 */
  margin: 0 10px 10px 0; /* 右下外边距设为 10px,留出间距  */
  width: 100px;
  height: 100px;
  background: #eee;
}

还不是预期结果,原因很简单:container 的宽度为 540px,由于右外边距的存在,导致每行右侧仅剩下 100px,该行余下空间容纳不了下一个元素(100px 宽度 + 10px 右外边框)。此时对 box 元素设置 marign-right: -10px 以增加其宽度至 550px,原本下一行的第一个元素就能浮上来了,然后再给 container 设置 overflow: hidden 截取掉溢出部分。我们还注意到最后一行也有一个 10px 的下外边距,同理借助 margin-bottom: -10px 使得文档流位置向上偏移就能很好地处理。

我们加上这样一个样式即可:

#box {
  margin-right: -10px;
  margin-bottom: -10px;
}

微信排版布局

利用负值 Margin 的特性,我们可以作一个「展开」的交互。如果结合零高特性,甚至可以做多次展开。

一个简单的示例:整体高度从原来的 600px,在点击触发 <svg> 的动画后变为 1200px,与此同时 <svg> 会隐藏可暴漏出前面绿、蓝的元素,以达到展开的效果。

<section style="width: 350px; margin: 0 auto; overflow: hidden; font-size: 0; line-height: 0">
  <section>
    <section style="height: 600px; background-color: green"></section>
    <section style="height: 600px; background-color: blue"></section>
  </section>
  <section style="margin-top: -1200px">
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 350 600" preserveAspectRatio="xMidYMin meet" style="width: 100%; max-width: none !important; background-color: red; pointer-events: none">
      <animate attributeName="width" begin="click" from="100%" to="200%" calcMode="linear" dur="2s" fill="freeze" restart="never" />
      <animate attributeName="opacity" begin="click" from="1" to="0" calcMode="linear" dur="0.001s" fill="freeze" restart="never" />
      <rect x="0" y="0" width="100%" height="100%" fill="transparent" style="pointer-events: visible">
        <set attributeName="visibility" begin="click" to="hidden" fill="freeze" restart="never" />
      </rect>
    </svg>
  </section>
</section>

References

The end.