Skip to content

盒模型

元素宽度的问题

接下来,你需要构建一个简单的网页:上面是网页头部,下面是两列内容,最终效果如下图所示。
我特意将这个网页设计成“块状”风格,这样就能清楚地看到所有元素的大小和位置。

一个头部加两列的布局

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<body>
  <header>
    <h1>Franklin Running Club</h1>
  </header>
  <div class="container">
    <main class="main">
      <h2>Come join us!</h2>
      <p>
        The Franklin Running club meets at 6:00pm every Thursday
        at the town square. Runs are three to five miles, at your
        own pace.
      </p>
    </main>
    <aside class="sidebar">
      <div class="widget"></div>
      <div class="widget"></div>
      <div class="gizmo"></div>
    </aside>
  </div>
</body>

该网页有一个头部、一个主元素和一个侧边栏,它们构成了网页的两列,并由一个容器元素包了起来。

下面处理一些基础样式。给网页设置字体,然后给网页和每个主要容器设置背景色,这样方便看清每个容器的位置和大小。完成这些工作,如图所示:

三个带背景的主要容器

在一些网站设计中,某些容器的背景色可能是透明的。在这种情况下,可以先暂时给容器设置一个背景色,等实现了容器的大小和位置后再去掉背景色。

样式代码如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!doctype html>
<head>
  <style>
    body {
      background-color: #eee;
      font-family: Helvetica, Arial, sans-serif;
    }

    header {
      color: #fff;
      background-color: #0072b0;
      border-radius: .5em;
    }

    .main {
      background-color: #fff;
      border-radius: .5em;
    }

    .sidebar {
      padding: 1.5em;
      background-color: #fff;
      border-radius: .5em;
    }

  </style>
</head>

由于现在侧边栏是空的,默认情况下没有高度,因此我们会给它加上内边距撑出一点高度。
其他容器最后也需要内边距,但稍后再处理。

接下来将两列放到合适的位置。我们首先使用浮动布局,将main和sidebar向左浮动,分别设置70%和30%的宽度。按照下面代码更新你的样式表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
.main {
    float: left;
    width: 70%;
    background-color: #fff;
    border-radius: .5em;
}

.sidebar {
    float: left;
    width: 30%;
    padding: 1.5em;
    background-color: #fff;
    border-radius: .5em;
}

结果如下图所示,并没有达到理想的效果:

设置main和sider的宽度

两列并没有并排出现,而是折行显示。
虽然将两列宽度设置为70%和30%,但它们总共占据的宽度超过了可用空间的100%,这是因为盒模型的默认行为。
当给一个元素设置宽或高的时候,指定的是内容的宽或高,所有内边距、边框、外边距都是追加到该宽度上的

默认盒模型

这种行为会让一个宽300px、内边距10px、边框1px的元素渲染出来宽322px(宽度加左右内边距再加左右边框)。
如果这些值使用不同的单位,情况就会更复杂。

在以上例子中,侧边栏的宽度等于30%宽度加上各1.5em的左右内边距,主容器的宽度只占70%。两列宽度加起来等于100%宽度加上3em。因为放不下,所以两列便折行显示了。

避免魔术数值

最笨的方法是减少其中一列(比如侧边栏)的宽度。
在我的屏幕上,侧边栏改为宽 26%,两列能够并排放下,但是这种方式不可靠。
26% 是一个魔术数值(magic number)。它不是一个理想的值,而是通过改样式试出来的值。

在编程中不推荐魔术数值,因为往往难以解释一个魔术数值生效的原因。
如果不理解这个数值是怎么来的,就不会知道在不同的情况下会产生什么样的结果。
我的屏幕宽1440px,在更小的视口下,侧边栏仍然会换行。
虽然CSS中有时确实需要反复试验,但目的是为了得到更好的样式,而不是为了强行将一个元素填入一个位置。

替代魔术数值的一个方法是让浏览器帮忙计算。
在本例中,因为加了内边距,两列的宽度总和超出了3em,所以可以使用 calc() 函数减去这个值,得到刚好 100% 的总和。
比如设置侧边栏宽度为 calc(30% -3em) 就能刚好并排放下两列,但是还有更好的解决办法。

调整盒模型

刚才遇到的问题说明默认的盒模型并不符合需求。
相反,我们需要让指定的宽度包含内边距和边框。在CSS中可以使用 box-sizing 属性调整盒模型的行为。

box-sizing 的默认值为 content-box,这意味任何指定的宽或高都只会设置内容盒子的大小。
将 box-sizing 设置为 border-box 后,height和width属性会设置内容、内边距以及边框的大小总和,这刚好符合示例的要求。

如图所示,盒模型的 box-sizing 为 border-box。在这个模型中,内边距不会让一个元素更宽,而是让内部的内容更窄。高度同理。

border-box盒模型

将这两个元素的box-sizing改为border-box就能在一行显示,不管左右内边距是多少(当30%小于左右内边距之和时会折行)

调整盒模型后两列并排

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
.main {
    box-sizing: border-box;
    float: left;
    width: 70%;
    background-color: #fff;
    border-radius: .5em;
}

.sidebar {
    box-sizing: border-box;
    float: left;
    width: 30%;
    padding: 1.5em;
    background-color: #fff;
    border-radius: .5em;
}

使用 box-sizing: border-box 后,两个元素加起来正好等于100%宽度。
现在因为它们70%和30%的宽度包含内边距,所以一行放得下两列。

全局设置 border-box

现在这两个元素的 box-sizing 更符合预期了,但是以后使用其他元素时还会遇到同样的问题。
最好能一次解决,这样以后就不用再想着调整盒模型了。下面代码用通用选择器()选中了页面上所有元素,并用两个选择器选中了网页的所有伪元素。
将这段代码放到你的样式表开头。

1
2
3
4
5
6
/* 给页面上所有元素和伪元素设置 border-box */
*,
::before,
::after {
    box-sizing: border-box;
}

加上这段代码,height和width会指定元素的实际宽和高。改变内边距不会影响它们。

说明:
将这段代码放到样式表开头已是普遍做法了。

但是,如果在网页中使用了带样式的第三方组件,就可能会因此破坏其中一些组件的布局,
尤其是当第三方组件在开发CSS的过程中没有考虑到使用者会修改盒模型时。
因为全局设置 border-box 时使用的通用选择器会选中第三方组件内的每个元素,修改盒模型可能会有问题,所以最终需要写另外的样式将组件内的元素恢复为 content-box

有一种简单点的方式,是利用继承改一下修改盒模型的方式。

1
2
3
4
5
6
7
8
9
:root {
    box-sizing: border-box;
}

*,
::before,
::after {
    box-sizing: inherit;
}

盒模型通常不会被继承,但是使用inherit关键字可以强制继承。
如下述代码所示,可以在必要时选中第三方组件的顶级容器,将其恢复为content-box。这样组件的内部元素会继承该盒模型。

1
2
3
.third-party-component {
  box-sizing: content-box;
}

现在网站上的每个元素都有一个可预测性更好的盒模型了。
如果要开发一个新的网站,建议将代码加到CSS中,因为从长远来看,这会给你省去很多麻烦。
但是如果给已有的样式表加上代码就可能有问题,尤其是当你已基于默认的内容盒模型写了很多样式后。
如果非要给已有项目加上这段代码,那么一定要彻底检查一遍看会不会有问题。

说明:

从现在开始,每个示例都假设你的样式表开头修改为了 border-box

给列之间加上间隔

通常在列之间加上一个小小的间隔会更好看。
这有时可以通过给一列加上内边距来实现,但有时这种方式行不通。
比如在示例中,两列都有背景色或者边框,这就需要将间隔放在两个元素的边框之间,如下图所示。

在两列之间加间隔

注意两个白色背景之间的灰色空间。实现这种效果有好几种方式,我们来介绍两种

首先,给其中一列加上外边距,再调整元素的宽度,将多出来的空间减掉。
代码从侧边栏的宽度中减掉了1%,将其增加到外边距上,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.main {
    float: left;
    width: 70%;
    background-color: #fff;
    border-radius: .5em;
}

.sidebar {
    float: left;
    width: 29%;
    margin-left: 1%;
    padding: 1.5em;
    background-color: #fff;
    border-radius: .5em;
}

这段代码的确加上了间隔,但是间隔的宽度由外层容器的宽度决定,百分比是相对于父元素的完整宽度的。
如果想用其他单位指定间距呢?(我更想用em指定间距,因为em单位的一致性更好。)可以用 calc() 来实现。

可以从宽度中减掉 1.5em 分给外边距,而不是完整宽度的 1%。代码中用calc()实现了这种效果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.main {
    float: left;
    width: 70%;
    background-color: #fff;
    border-radius: .5em;
}

.sidebar {
    float: left;
    width: calc(30% - 1.5em);
    margin-left: 1.5em;
    padding: 1.5em;
    background-color: #fff;
    border-radius: .5em;
}

这种方式不仅能够使用em指定间距,而且能让代码意图更明显。
之后再看代码,从第一个代码清单中可能看不出为什么使用29%,但是第二个代码清单中的 30% -1.5em 则能提供线索,知道它是基于 30% 算出来的。

元素高度的问题

处理元素高度的方式跟处理宽度不一样。之前对border-box的修改依然适用于高度,而且很有用,但是通常最好避免给元素指定明确的高度。
普通文档流是为限定的宽度和无限的高度设计的。内容会填满视口的宽度,然后在必要的时候折行。
因此,容器的高度由内容天然地决定,而不是容器自己决定。

普通文档流——指的是网页元素的默认布局行为。
行内元素跟随文字的方向从左到右排列,当到达容器边缘时会换行。块级元素会占据完整的一行,前后都有换行。

控制溢出行为

当明确设置一个元素的高度时,内容可能会溢出容器。当内容在限定区域放不下,渲染到父元素外面时,就会发生这种现象。
下图展示了这一现象。文档流不考虑溢出的情况,其容器下方的任何内容都会渲染到溢出内容的上面。

内容溢出了容器

用overflow属性可以控制溢出内容的行为,该属性支持以下4个值。

  • visible(默认值)——所有内容可见,即使溢出容器边缘。
  • hidden——溢出容器内边距边缘的内容被裁剪,无法看见。
  • scroll——容器出现滚动条,用户可以通过滚动查看剩余内容。在一些操作系统上,会出现水平和垂直两种滚动条,即使所有内容都可见(不溢出)。不过,在这种情况下,滚动条不可滚动(置灰)。
  • auto——只有内容溢出时容器才会出现滚动条。

通常情况下,我倾向于使用auto而不是scroll,因为在大多数情况下,我不希望滚动条一直出现。
下图展示了4个overflow值对应的4个容器。

4个overflow值对应的4个容器

从左到右,overflow 值分别是 visible、hidden、scroll、auto

谨慎地使用滚动条。浏览器给网页最外层加上了滚动条,如果网页内部再嵌套滚动区域,用户就会很反感
如果用户使用鼠标滚轮滚动网页,当鼠标到达一个较小的滚动区域,滚轮就会停止滚动网页,转而滚动较小的区域。

水平方向的溢出:

除了垂直溢出,内容也可能在水平方向溢出。一个典型的场景就是在一个很窄的容器中放一条很长的URL。溢出的规则跟垂直方向上的一致。

可以用 overflow-x 属性单独控制水平方向的溢出,或者用 overflow-y 控制垂直方向溢出。
这些属性支持overflow的所有值,然而同时给x和y指定不同的值,往往会产生难以预料的结果。

百分比高度的备选方案

用百分比指定高度存在问题。百分比参考的是元素容器块的大小,但是容器的高度通常是由子元素的高度决定的。
这样会造成死循环,浏览器处理不了,因此它会忽略这个声明。要想让百分比高度生效,必须给父元素明确定义一个高度。

人们使用百分比高度是想让一个容器填满屏幕。不过更好的方式是用视口的相对单位vh,前面已经介绍过。100vh等于视口的高度。
还有一个更常见的用法是创造等高列。这不用百分比也能实现。

等高列

等高列的问题从CSS出现就一直困扰着人们。在21世纪初,CSS取代了HTML表格成为布局的主要方式。
当时表格是实现等高列的唯一方式,更具体地说,是不明确指定高度就能实现等高列的唯一方式,
虽然可以简单地将所有列设置高度 500px 或者其他任意值,但是如果要让列自己决定高度,每个元素可能算出来都不一样高,具体高度取决于内容。
这个简单的用例就足以让人们抓狂。

为了解决这个问题,诞生了很多有创意的解决方案。随着CSS的演进,出现了伪元素、负外边距等方案。
如果你还在用这些复杂的方案,那么是时候改变了。
现代浏览器支持了CSS表格,可以轻松实现等高列,比如IE8+支持 display: table, IE10+支持弹性盒子或者Flexbox,都默认支持等高列。

说明
现代浏览器是指能够自动更新(长青)的浏览器的最近版本,包括 Chrome、Firefox、Edge、Opera 还有 Safari。不再考虑 IE

很多常见的设计需要等高列,比如两列布局就是个典型的例子。
如果将主列和侧边栏的高度对齐(如下图所示),看起来就会更精致。任意一列的内容增加,两列的高度都会增加,同时保持底部对齐。

等高列

当然,你可以给两列随便设置一个高度值,但是应该选择什么值呢?太大了就会在容器底部留下大片空白,太小了内容就会溢出。

最好的办法是让它们自己决定高度,然后扩展较矮的列,让它的高度等于较高的列。下面会演示通过CSS表格和Flexbox两种方式实现这种效果。

CSS 表格布局

首先,用CSS表格布局替代浮动布局。给容器设置 display: table,给每一列设置 display: table-cell
按照代码清单更新样式。(你可能注意到了这里没有 table-row 元素,因为CSS表格不像HTML表格那样必须有行元素。)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.container {
    /* 让容器布局像表格一样 */
    display: table;
    width: 100%;
}

/* 让列布局像表格的单元格一样 */
.main {
    display: table-cell;
    width: 70%;
    background-color: #fff;
    border-radius: .5em;
}

.sidebar {
    display: table-cell;
    width: 30%;
    /* 外边距不再生效 */
    margin-left: 1.5em;
    padding: 1.5em;
    background-color: #fff;
    border-radius: .5em;
}

不像 block 的元素,默认情况下,显示为 table 的元素宽度不会扩展到 100%,因此需要明确指定宽度。
以上代码已经差不多实现了需求,但是缺少间隔。这是因为外边距并不会作用于table-cell元素,所以要修改代码,让间隔生效。

可以用表格元素的border-spacing属性来定义单元格的间距。
该属性接受两个长度值:水平间距和垂直间距。(也可以将这两个长度值指定为同一值。)
可以给容器加上 border-spacing: 1.5em 0,但这会产生一个特殊的副作用:这个值也会作用于表格的外边缘。
这样两列就无法跟头部左右对齐了(如图所示)。

使用border-spacing

机智的你可能会想到负外边距,但是这需要给整个表格包裹一层新的容器。
具体步骤:在表格容器外面包一个元素<div class="wrapper">,将其左右外边距设置为−1.5em,从而抵消表格容器外侧1.5em的border-spacing。样式表如代码清单所示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<div class="wrapper">
<div class="container">
    <main class="main">
    <h2>Come join us!</h2>
    <p>
        The Franklin Running club meets at 6:00pm every Thursday
        at the town square. Runs are three to five miles, at your
        own pace.
    </p>
    </main>
    <aside class="sidebar">
    <div class="widget"></div>
    <div class="widget"></div>
    <div class="gizmo"></div>
    </aside>
</div>
</div>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* 添加一个新的包裹元素,设置负外边距 */
.wrapper {
    margin-left: -1.5em;
    margin-right: -1.5em;
}

.container {
    display: table;
    width: 100%;
    /* 单元格之间加上水平的 border-spacing */
    border-spacing: 1.5em 0;
}

.main {
    display: table-cell;
    width: 70%;
    background-color: #fff;
    border-radius: .5em;
}

.sidebar {
    display: table-cell;
    width: 30%;
    padding: 1.5em;
    background-color: #fff;
    border-radius: .5em;
}

正的外边距会将容器的边缘往里推,而负的外边距则会将边缘往外拉。
结合border-spacing,两列靠近外侧的边缘跟<body>(包裹元素所在的容器盒子)的边缘对齐了。
现在的布局满足了需求:两列等高,1.5em的间距,外边缘跟头部对齐

等高列

用表格实现布局

如果你已经做了一段时间Web开发,大概听过用HTML表格实现布局并非明智之举。
在21世纪初,很多网站设计师使用<table>元素实现网站布局,因为它比用浮动布局(当时唯一的替代方案)简单。
后来,有许多人强烈反对表格布局,因为它用了无语义的HTML标签。那时表格没有承担内容标签的功能,反而做着本该由CSS负责的布局工作。

浏览器现在支持将各种元素显示为表格,而不只是<table>,因此我们可以一边享受表格布局带来的好处,一边维护语义标记。
然而这种方式并不是完美的解决方案,HTML表格的 colspan 和 rowspan 属性在CSS中没有可替代的方案,而且 浮动、Flexbox 以及 inline-block 可以实现表格无法实现的布局。

Flexbox

我们还可以用Flexbox实现两列等高布局,如下面代码清单所示。
Flexbox不需要一个额外的div包裹元素,它默认会产生等高的元素。此外也不需要使用负外边距。

提示如果你不用支持IE9及其以下的浏览器,建议使用Flexbox而不是表格布局。
将div包裹元素去掉,按照代码清单更新样式表。如果你还不了解Flexbox,下面有简要介绍。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
.container {
    /* 将容器的 display 属性设置为 flex */
    display: flex;
}

/* 弹性容器内的元素不需要指定 display 或者 float 属性 */
.main {
    width: 70%;
    background-color: #fff;
    border-radius: .5em;
}

.sidebar {
    width: 30%;
    padding: 1.5em;
    /* 跟浮动布局一样,外边距可以生效 */
    margin-left: 1.5em;
    background-color: #fff;
    border-radius: .5em;
}

给容器设置display: flex,它就变成了一个弹性容器(flex container),子元素默认等高。
你可以给子元素设置宽度和外边距,尽管加起来可能超过100%, Flexbox也能妥善处理。
以上代码清单渲染出来的样式跟表格布局一样,而且不需要额外包裹元素,CSS也更简单。

Flexbox提供了很多选项。这个例子里的选项已足以创建你的第一个Flexbox的布局了。

警告:
除非别无选择,否则不要明确设置元素的高度。先寻找一个替代方案。设置高度一定会导致更复杂的情况。

使用 min-height 和 max-height

接下来介绍的两个是很有用的属性:min-heightmax-height
你可以用这两个属性指定最小或最大值,而不是明确定义高度,这样元素就可以在这些界限内自动决定高度。

如果你想要将一张大图放在一大段文字后面,但是担心它溢出容器,就可以用 min-height 指定一个最小高度,而不指定它的明确高度。
这意味着元素至少等于你指定的高度,如果内容太多,浏览器就会允许元素自己扩展高度,以免内容溢出。

下图有三个元素。左边的元素没有 min-height,因此它的高度由自身决定,另外两个元素都设置了 min-height 为 3em。
中间的元素如果自己决定高度的话应该比现在矮,但是min-height值让它的高度为3em。右边的元素内容多到已经超过3em,容器自然地扩展高度,以容纳内容。

三个元素,一个没有指定高度,另外两个元素设置了 min-height 为3em:

min-height示例

同理,max-height允许元素自然地增高到一个特定界限。
如果到达这个界限,元素就不再增高,内容会溢出。
还有类似的属性是min-width和max-width,用于限制元素的宽度

垂直居中内容

CSS另一个让人头疼的问题就是垂直居中。过去有好几种方式实现垂直居中,但是每一种方式都有一定的局限性。
在CSS中,回答一个问题的答案通常是“这得看情况”,垂直居中就是如此。

为什么vertical-align不生效:

如果开发人员期望给块级元素设置 vertical-align: middle 后,块级元素里的内容就能垂直居中,那么他们通常会失望,因为浏览器会忽略这个声明。

vertical-align 声明只会影响 行内元素 或者 table-cell 元素。
对于行内元素,它控制着该元素跟同一行内其他元素之间的对齐关系。
比如,可以用它控制一个行内的图片跟相邻的文字对齐。

对于显示为 table-cell 的元素,vertical-align 控制了内容在单元格内的对齐。
如果你的页面用了CSS表格布局,那么可以用 vertical-align 来实现垂直居中。

一个不好的做法就是,给容器设定一个高度值,然后试图让动态大小的内部元素居中。在实现这种效果时,请尽量交给浏览器来决定高度。

CSS中最简单的垂直居中方法是给容器相等的上下内边距,让容器和内容自行决定自己的高度(如下图所示),对应的样式见代码。
你可以暂时将这段代码放到样式表,看看它在网页中的样式(之后记得删掉,因为在设计中不是这样)。

使用内边距让内容垂直居中:

使用内边距让内容垂直居中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!doctype html>
<head>
  <style>
    :root {
      box-sizing: border-box;
    }
    *,
    ::before,
    ::after {
      box-sizing: inherit;
    }
    body {
      background-color: #eee;
      font-family: Helvetica, Arial, sans-serif;
    }
    header {
      padding-top: 4em;
      padding-bottom: 4em;
      color: #fff;
      background-color: #0072b0;
      border-radius: .5em;
    }

  </style>
</head>

<body>
  <header>
    <h1>Franklin Running Club</h1>
  </header>
</body>

不管容器里的内容显示为行内、块级或者其他形式,这种方法都有效,
但有时我们想给容器设置固定高度,或者无法使用内边距,因为想让容器内另一个子元素靠近容器的顶部或者底部。

这在等高列中也是一个常见的问题,尤其是用浮动布局这种较传统的技术实现时。
还好CSS表格和Flexbox能够轻松实现居中。(如果用传统的技术,需要用别的办法处理内容居中。)
不同的情况有不同的处理方法,具体参考如下面的附加栏所示。

垂直居中指南

在容器里让内容居中最好的方式是根据特定场景考虑不同因素。
做出判断前,先逐个询问自己以下几个问题,直到找到合适的解决办法。其中一些技术会在后面介绍,可根据情况翻阅对应的内容寻找答案。

  • 可以用一个自然高度的容器吗? 给容器加上相等的上下内边距让内容居中。
  • 容器需要指定高度或者避免使用内边距吗? 对容器使用 display: table-cellvertical-align: middle
  • 可以用Flexbox吗? 如果不需要支持IE9,可以用Flexbox居中内容。
  • 容器里面的内容只有一行文字吗? 设置一个大的行高,让它等于理想的容器高度。这样会让容器高度扩展到能够容纳行高。如果内容不是行内元素,可以设置为 inline-block
  • 容器和内容的高度都知道吗? 将内容绝对定位。
  • 不知道内部元素的高度? 用绝对定位结合变形(transform)。

还不确定的话,参考 howtocenterincss
网站。这个网站很不错,可以根据自己的场景填写几个选项,然后它会相应地生成垂直居中的代码。

负外边距

不同于内边距和边框宽度,外边距可以设置为负值。
负外边距有一些特殊用途,比如让元素重叠或者拉伸到比容器还宽。

负外边距的具体行为取决于设置在元素的哪边,如图所示。
如果设置左边或顶部的负外边距,元素就会相应地向左或向上移动,导致元素与它前面的元素重叠,
如果设置右边或者底部的负外边距,并不会移动元素,而是将它后面的元素拉过来。
给元素底部加上负外边距并不等同于给它下面的元素顶部加上负外边距。

负外边距的行为

如果不给一个块级元素指定宽度,它会自然地填充容器的宽度。
但如果在右边加上负外边距,则会把它拉出容器。
如果在左边再加上相等的负外边距,元素的两边都会扩展到容器外面。
这就是为什么可以拉宽之前图里的表格容器布局,让它填满<body>的宽度,忽略border-spacing的影响。

警告
如果元素被别的元素遮挡,利用负外边距让元素重叠的做法可能导致元素不可点击。

负外边距并不常用,但是在某些场景下很实用,尤其是当创建列布局的时候。
不过应当避免频繁使用,不然网页的样式就会失控。

外边距折叠

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<!doctype html>
<head>
  <style>
    :root {
      box-sizing: border-box;
    }

    *,
    ::before,
    ::after {
      box-sizing: inherit;
    }

    body {
      background-color: #eee;
      font-family: Helvetica, Arial, sans-serif;
    }

    header {
      color: #fff;
      background-color: #0072b0;
      border-radius: .5em;
    }

    .container {
      display: flex;
    }

    .main {
      width: 70%;
      background-color: #fff;
      border-radius: .5em;
    }

    .sidebar {
      width: 30%;
      padding: 1.5em;
      margin-left: 1.5em;
      background-color: #fff;
      border-radius: .5em;
    }

  </style>
</head>

<body>
  <header>
    <h1>Franklin Running Club</h1>
  </header>
  <div class="container">
    <main class="main">
      <h2>Come join us!</h2>
      <p>
        The Franklin Running club meets at 6:00pm every Thursday
        at the town square. Runs are three to five miles, at your
        own pace.
      </p>
    </main>
    <aside class="sidebar">
      <div class="widget"></div>
      <div class="widget"></div>
      <div class="gizmo"></div>
    </aside>
  </div>
</body>

再看一眼你的网页,有没有发现有些外边距不对劲?头部和容器明明没有添加任何外边距,为什么它们之间会有间距呢?

外边距折叠导致了间距

当顶部和/或底部的外边距相邻时,就会重叠,产生单个外边距。这种现象被称作折叠。
上图中头部下方的空白就是由于外边距折叠造成的。下面看看它的工作原理。

文字折叠

外边距折叠的主要原因与包含文字的块之间的间隔相关。
段落(<p>)默认有1em的上外边距和1em的下外边距。
这是用户代理的样式表添加的,但当前后叠放两个段落时,它们的外边距不会相加产生一个2em的间距,而会折叠,只产生1em的间隔。

比如,示例中左列里的文字块就发生了外边距折叠。
<h2>标题(“Come join us! ”)底部的外边距为 0.83em,跟它后面的段落的顶部外边距折叠。
如下图所示,分别是它们的外边距。注意每个元素的外边距是如何在网页中占据相同位置的。

标题和段落的外边距

折叠外边距的大小等于相邻外边距中的最大值。
标题的下方有19.92px的外边距(24px字号× 0.83em外边距),段落顶部有16px的外边距(16px字号×1em外边距)。
它们中的较大者是19.92px,也就是最终渲染的两个元素之间的间距。

多个外边距折叠

即使两个元素不是相邻的兄弟节点也会产生外边距折叠。
即使将这个段落用一个额外的div包裹起来,如代码所示,最终视觉结果也一样。
在没有其他CSS的影响下,所有相邻的顶部和底部外边距都会折叠。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<main class="main">
    <h2>Come join us!</h2>
    <div>
    <p>
        The Franklin Running club meets at 6:00pm every Thursday
        at the town square. Runs are three to five miles, at your
        own pace.
    </p>
    </div>
</main>

在代码中,有三个不同的外边距折叠到一块了:<h2> 底部的外边距、<div> 顶部的外边距、<p> 顶部的外边距。
计算值分别是19.92px、0px、16px。因此最终间隔还是19.92px,也就是三者中最大的值。
实际上,即使将段落放在多个div中嵌套,渲染结果都一样:所有的外边距都会折叠到一起。

总之,所有相邻的顶部和底部外边距会折叠到一起。如果在页面中添加一个空的、无样式的div(没有高度、边框和内边距),它自己的顶部和底部外边距就会折叠。

说明
只有上下外边距会产生折叠,左右外边距不会折叠。

折叠外边距就像“个人空间”。如果在公交车站站着两个人,他们每个人都认为较为舒适的个人空间应为3英尺,那么他们就会乐意间隔3英尺,而不必间隔6英尺才让双方满意。

这也就是说可以给任何元素加上外边距,而不必担心它们前后的元素是什么。
如果给标题底部加上 1.5em 的外边距,那么在标题下面不管是顶部外边距为 1em 的<p>元素还是没有顶部外边距的div元素,它们之间的间距都等于1.5em。只有当后面的元素需要更大的空间时,折叠外边距才会大于1.5em。

容器外部折叠

三个连续的外边距折叠可能会让你措手不及。
如果元素的容器有背景色,元素的外边距在容器外面折叠通常会产生不想要的效果。

再看看图中头部下面的间距。网页标题是 <h1>,用户代理样式表给它底部设置的外边距为0.67em(21.44px)。
它的父元素是<header>,没有设置任何外边距。
因为它们的底部外边距相邻,所以会折叠,导致<header>下方出现了21.44px的外边距。这两个元素顶部的外边距也发生了折叠。

这种现象比较奇怪。在这种情况下,我们希望 <h1> 的外边距留在 <header> 中,但是外边距不一定在理想的地方折叠。
幸运的是,有一些方法可以防止这种现象。实际上你已经在网页的主区域修复过这个问题了。
注意看“Come join us! ”上方的外边距没有在容器外面折叠,这是因为弹性子元素的外边距不会折叠,而这一块刚好用了Flexbox布局。

内边距也能解决这个问题。给头部添加上下内边距,外边距就不会在容器外部折叠。
现在将头部的样式更新,让它如下图所示,也加上左右内边距。
如代码所示更新你的样式表,但是这样会丢失头部和主内容之间的外边距,我们稍后会解决这个问题。

给头部加上内边距防止外边距折叠

1
2
3
4
5
6
header {
    padding: 1em 1.5em;
    color: #fff;
    background-color: #0072b0;
    border-radius: .5em;
}

如下方法可以防止外边距折叠。

  • 对容器使用 overflow: auto(或者非visible的值),防止内部元素的外边距跟容器外部的外边距折叠。这种方式副作用最小。
  • 在两个外边距之间加上边框或者内边距,防止它们折叠。
  • 如果容器为浮动元素、内联块、绝对定位或固定定位时,外边距不会在它外面折叠。
  • 当使用Flexbox布局时,弹性布局内的元素之间不会发生外边距折叠。网格布局同理。
  • 当元素显示为table-cell时不具备外边距属性,因此它们不会折叠。此外还有table-row和大部分其他表格显示类型,但不包括table、table-inline、table-caption。

这些方法中有很多会改变元素的布局行为,除非它们能产生想要的布局,否则不要轻易使用。

容器内的元素间距

容器的内边距和内容的外边距之间的相互作用处理起来很棘手。
我们给侧边栏加上一些元素,看看会出现什么问题以及如何解决。最后,我会介绍一个实用技术来简化这些问题。

我们将给侧边栏加上两个跳转到社交媒体页的按钮以及一个不重要的链接。侧边栏最终完成的效果如图所示。

侧边栏里的内容有适当的间距

先从两个社交链接入手。如代码所示,给侧边栏加上两个链接,用button-link类作为选择器。

1
2
3
4
<aside class="sidebar">
    <a href="/twitter" class="button-link">follow us on Twitter</a>
    <a href="/facebook" class="button-link">like us on Facebook</a>
</aside>

接下来,要给按钮加上通用样式。将其设置为块级元素,以便填满容器的宽度,也能够让每个按钮单独一行。将代码加入你的样式表。

1
2
3
4
5
6
7
8
9
.button-link {
    display: block;
    padding: .5em;
    color: #fff;
    background-color: #0090C9;
    text-align: center;
    text-decoration: none;
    text-transform: uppercase;
}

完成了这两个链接的样式后,还需要加上间距。
此刻,没有外边距的它们直接上下堆叠在一起。
现在有两个选择:分别或同时指定它们的上下外边距,两个按钮之间会发生外边距折叠。

然而不管选择哪种方式,都会遇到一个问题:侧边栏的内边距需要跟按钮的外边距接触。如果加上 margin-top: 1.5em,最终效果如下图所示。

按钮的上外边距使得容器的内边距看起来更大了

现在容器顶部有了多余的空间。第一个按钮的顶部外边距加上容器顶部的内边距产生的空间大于其余三个方向的留白,显得很不匀称。

有好几种方法可以解决该问题。代码清单是其中较简单的办法。它使用相邻的兄弟组合器(+)选中同一个父元素下紧跟在其他button-link 后面的 button-link 元素。
现在只在两个按钮之间存在外边距。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
.button-link {
    display: block;
    padding: .5em;
    color: #fff;
    background-color: #0090C9;
    text-align: center;
    text-decoration: none;
    text-transform: uppercase;
}

.button-link + .button-link {
    margin-top: 1.5em;
}

这看起来生效了(如图所示)。第一个按钮不再有顶部外边距,按钮四周的间距一致。

按钮四周加上了一致的间距

如果内容改变了

上述方法让一切如预期,但是如果在侧边栏添加更多内容,则会再次出现间距问题。
如代码清单所示,加上第三个链接,类设为sponsor-link,这样就可以给链接加上其他样式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<aside class="sidebar">
    <a href="/twitter" class="button-link">
    follow us on Twitter
    </a>
    <a href="/facebook" class="button-link">
    like us on Facebook
    </a>
    <a href="/sponsors" class="sponsor-link">
    become a sponsor
    </a>
</aside>

接下来需要给这个链接添加样式,但是同时还得处理它跟其他按钮的间距。下图是处理之前的样子。

第二个按钮和底部链接之间缺少间距

代码清单是底部链接的样式。将它们添加到你的样式表。你可能想给链接也加一个顶部外边距,且慢,我接下来会介绍另一个有趣的方案。

1
2
3
4
5
6
.sponsor-link {
    display: block;
    color: #0072b0;
    font-weight: bold;
    text-decoration: none;
}

虽然给链接加上顶部外边距也可以实现效果,但是考虑到HTML会频繁改动,也许下个月,也许下一年,总有一天这个侧边栏里有些内容需要移动或者替换。
可能赞助商链接会需要移动到侧边栏的顶部,或者需要添加一个注册邮件简报的组件。

每一次改变HTML,都需要考虑这些外边距的问题。
你得确保每个元素之间有间距,但是容器的顶部(或底部)没有多余的间距。

更通用的解决方案:猫头鹰选择器

Web设计师Heydon Pickering曾表示外边距“就像是给一个物体的一侧涂了胶水,而你还没有决定是否要将它贴到某处,或者还没想好要贴到什么东西上”。
不要给网页当前的内容固定外边距,而是应该采取更通用的方式,不管网页结构如何变化都能够生效。
这就是Heydon Pickering所说的迟钝的猫头鹰选择器(lobotomized owl selector)(以下简称猫头鹰选择器),因为它长这样:* + *

该选择器开头是一个通用选择器(),它可以选中所有元素,后面是一个相邻兄弟组合器(+),最后是另一个通用选择器。
它因形似一只眼神空洞的猫头鹰而得名。
猫头鹰选择器功能接近此前介绍的选择器:.social-button + .social-button,但是它不会选中直接跟在其他按钮后面的按钮,而是会选中直接跟在其他元素后面的任何元素。
也就是说,它会选中页面上有着相同父级的非第一个子元素。

接下来用猫头鹰选择器给页面元素加上顶部外边距。
这样就会给侧边栏的每一个元素加上一致的间距。该选择器还会选中主容器,因为它是头部的相邻兄弟节点,也如你所愿加上了间距。效果如图所示。

所有的相邻兄弟元素都有顶部外边距

将代码清单添加到你的样式表的开头。我将body放在选择器的前面,这样该选择器就只能选中body内的元素。
如果直接使用猫头鹰选择器,它还会选中<body>元素,因为它是<head>元素的相邻兄弟节点。

1
2
3
body * + * {
    margin-top: 1.5em;
}

说明
你也许会担心通用选择器()的性能问题。
现在不必担心了,因为现代浏览器都能很好地处理。
此外,猫头鹰选择器可能会减少样式表中的选择器数量,因为它在全局范围内处理了大多数元素的间距问题。
实际上,如果你的样式表要处理的细节很多的话,猫头鹰选择器的性能可能更好。

猫头鹰选择器的顶部外边距对侧边栏有个副作用。
因为侧边栏是主列的相邻兄弟元素,所以它也会有顶部外边距。
因此要将其恢复为0,还需要给主列补上内边距。根据代码清单更新你的样式表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
.main {
    width: 70%;
    /* 给主列加上内边距 */
    padding: 1em 1.5em;
    background-color: #fff;
    border-radius: .5em;
}

.sidebar {
    width: 30%;
    padding: 1.5em;
    /* 移除猫头鹰选择器设置的顶部外边距 */
    margin-top: 0;
    margin-left: 1.5em;
    background-color: #fff;
    border-radius: .5em;
}

完成最终样式后,页面如图所示:

两列布局的最终页面

这样使用猫头鹰选择器是需要权衡的。它省去了许多的需要设置外边距的地方,但是在某些不想加外边距的地方则需要覆盖。
通常只在有并列元素,或者有多列布局时这样使用。有时还需要根据设计,给段落和标题设置特定的外边距。

接下来会有更多的例子使用猫头鹰选择器,帮助你理解它何时需要权衡。猫头鹰选择器可能并非是所有项目的正确选择,而且很难添加到已有项目中,因为可能破坏已有布局,不过可以在下次开始做新网站或Web应用程序时考虑使用它。

完整的代码如下所示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<!doctype html>
<head>
  <style>
    :root {
      box-sizing: border-box;
    }

    *,
    ::before,
    ::after {
      box-sizing: inherit;
    }

    body {
      background-color: #eee;
      font-family: Helvetica, Arial, sans-serif;
    }

    body * + * {
      margin-top: 1.5em;
    }

    header {
      padding: 1em 1.5em;
      color: #fff;
      background-color: #0072b0;
      border-radius: .5em;
    }

    .container {
      display: flex;
    }

    .main {
      width: 70%;
      padding: 1em 1.5em;
      background-color: #fff;
      border-radius: .5em;
    }

    .sidebar {
      width: 30%;
      padding: 1.5em;
      margin-top: 0;
      margin-left: 1.5em;
      background-color: #fff;
      border-radius: .5em;
    }

    .button-link {
      display: block;
      padding: .5em;
      color: #fff;
      background-color: #0090C9;
      text-align: center;
      text-decoration: none;
      text-transform: uppercase;
    }

    .sponsor-link {
      display: block;
      color: #0072b0;
      font-weight: bold;
      text-decoration: none;
    }

  </style>
</head>

<body>
  <header>
    <h1>Franklin Running Club</h1>
  </header>
  <div class="container">
    <main class="main">
      <h2>Come join us!</h2>
      <p>
        The Franklin Running club meets at 6:00pm every Thursday
        at the town square. Runs are three to five miles, at your
        own pace.
      </p>
    </main>
    <aside class="sidebar">
      <a href="/twitter" class="button-link">
        follow us on Twitter
      </a>
      <a href="/facebook" class="button-link">
        like us on Facebook
      </a>
      <a href="/sponsors" class="sponsor-link">
        become a sponsor
      </a>
    </aside>
  </div>
</body>

总结

  • 总是全局设置border-box,以便得到预期的元素大小。
  • 避免明确设置元素的高度,以免出现溢出问题。
  • 使用现代的布局技术,比如 display: table 或者Flexbox实现列等高或者垂直居中内容。
  • 如果外边距的行为很奇怪,就采取措施防止外边距折叠。
  • 使用猫头鹰选择器全局设置堆叠元素之间的外边距。

其他

内边距 padding

CSS 允许你使用 padding-top、padding-right、padding-bottom 和 padding-left 来控制元素上右下左四个方向的 padding

除了分别指定元素的 padding-top、padding-right、padding-bottom 和 padding-left 属性外,你还可以集中起来指定它们,举例如下:

padding: 10px 20px 10px 20px;

这四个值以顺时针方式排列:顶部、右侧、底部、左侧,简称:上右下左。

外边距 margin

控制元素边框 border 和元素实际所占空间的距离, 如果将一个元素的 margin 设置为负值,元素将会变大

CSS 允许你使用 margin-top、margin-right、margin-bottom 和 margin-left 来控制元素上右下左四个方向的 margin

边框 border

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
.box1 {
    /* 盒子框 */
    /* border: 1px solid black; */
    /* 内边距 */
    /* 上下左右 10px */
    /* padding: 10px; */
    /* 上下20px 左右50px */
    /* padding: 20px 50px; */
    /* 上20px 左右50px 下60px */
    /* padding: 20px 50px 60px; */
    /* 上20px 右30px 下40px 左50px */
    padding: 20px 30px 40px 50px;
    /* padding-top: 20px;
    padding-bottom: 30px;
    padding-left: 40px;
    padding-right: 50px; */
    /* 背景不包含边框 */
    background: yellow;
    /* 外边距 */
    /* margin: 30px 50px; */
    margin: 30px;

    /* overflow: hidden; */
}
div {
    /* float: left; */
    /* display: inline-block; */
    width: 800px;
    /* border: 1px solid black; */
}
.box2 {
    /* border: 5px solid black; */
    background: green;
    margin: 60px;
}
/* 
margin 重叠
<div class="box1">div1</div>
<!-- <br> -->
<div class="box2">div2</div>
1 平级的元素 取最大值
    - float
    - inline-block
<div class="box1">
    <!-- div1 -->
    <div class="box2">div2</div>
    <!-- div1 -->
</div>
2 嵌套关系 
    - border
    - padding
    - overflow
*/

display/position

确定元素的显示类型

display: blcok/inline/inline-block

确定元素的位置

position: static/relative/absolute/fixed

absolute/fixed 脱离了文档流

absolute相对于最近的父级元素

fixed 相对于屏幕(可视区域)去变

z-index,确定层级,谁的数值高,谁覆盖