Skip to content

盒模型

::: theorem 本章概要 ❑ 给元素设置大小的实用经验
❑ 实现垂直居中
❑ 实现等高列
❑ 负的外边距和外边距折叠
❑ 网页组件之间一致的间距
:::

本章我们要打好基础,深刻理解浏览器是如何设置元素的大小和位置的。高级的布局话题基于文档流和盒模型等概念,这些是决定网页元素的大小和位置的基本规则。之后几章会详细介绍几种布局技术。

本章将构建一个两列布局的网页,它是一个经典的 CSS 入门练习。

元素宽度的问题

两列布局

HTML
<body>
  <header>
    <h1>Franklin Running Club</h1>
  </header>
  <div class="container">
    <main class="main">
      <h2>Come join us!</h2>
      <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatem, voluptate porro, rem labore distinctio ab magnam odio numquam impedit eligendi voluptas mollitia et accusantium dolor?</p>
    </main>
    <aside class="sidebar">
      <div class="widget"></div>
    </aside>
  </div>
</body>
CSS
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;
}

使用浮动布局,将 main 和 sidebar 向左浮动,分别设置 70% 和 30% 的宽度。

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

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

两列并没有并排出现,而是折行显示。

避免魔术数值

设置侧边栏宽度为 calc(30% - 3em) 就能刚好并排放下两列,但是还有更好的解决办法。

调整盒模型

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

CSS
.main {
  background-color: #fff;
  border-radius: .5em;
  float: left;
  width: 70%;
  box-sizing: border-box;
}

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

全局设置 border-box

CSS
: root {
  box-sizing: border-box;
}

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

盒模型通常不会被继承,但是使用 inherit 关键字可以强制继承。

从现在开始,本笔记后续内容的每个示例都假设你的样式表开头修改为了 border-box

注意:

  1. 全局设置 border-box 在 Typora 主题制作中并不需要,因为软件作者在 base.css 已经预先做了类似的处理
  2. 也有观点认为不应该全局重置,只需部分重置即可:
    css
    input,
    textarea,
    img,
    video,
    object {
      box-sizing: border-box;
    }

给列之间加上间隔

实现这种效果有好几种方式,我们来介绍两种方法。

共同之处:给其中一列加上外边距,再调整元素的宽度,将多出来的空间减掉

不同之处:间距可变与间距不可变

  • margin-left: 1%: 间隔的宽度由外层容器的宽度决定,因为百分比是相对于父元素的宽度计算 ( 间距随视口宽度变化 )
  • margin-left: 1.5em: 用 em 指定间距,因为 em 单位的一致性更好 ( 间距不随视口变化,因为元素字号固定 1.5em )

方法一:基于百分比的外边距留白,父容器的宽度的 1%

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

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

方法二:基于 em 指定间隔,取 24px

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

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

元素的高度问题

通常最好避免给元素指定明确的高度。普通文档流是为限定的宽度和无限的高度设计的。内容会填满视口的宽度,然后在必要的时候折行。因此,容器的高度由内容天然地决定,而不是容器自己决定。

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

当明确设置一个元素的高度时,内容可能会溢出容器。

控制溢出行为

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

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

百分比高度的备选方案

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

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

等高列

很多常见的设计需要等高列,比如本章的两列布局就是个典型的例子。任意一列的内容增加,两列的高度都会增加,同时保持底部对齐。

最好的办法是让它们自己决定高度,然后扩展较矮的列,让它的高度等于较高的列。此时的页面设计,侧边栏没有内容,高度是塌陷的,并不是等高列。接下来是实现等高列的两种方法。

CSS 表格布局

用 CSS 表格布局替代浮动布局:给容器设置 display: table,给每一列设置 display: table-cell,即可实现等高列。

CSS
.container {
  display: table;
  width: 100%;
}

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

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

外边距并不会作用于 table-cell 元素,所以要修改使用 border-spacing,在单元格之间加上水平的间隔,再对外部容器使用负外边距将内容向两边拉扯、平移、对齐。

CSS
.wrapper {
  margin-left: -1.5em;
  margin-right: -1.5em;
}

.container {
  display: table;
  border-spacing: 1.5em 0;
}

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

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

Flexbox

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

CSS
.container {
  display: flex;
}

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

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

警告

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

使用 min-heightmax-height

min-height 指定一个最小高度,默认最大高度不限,如果内容太多,浏览器就会允许元素自己扩展高度,避免内容溢出。

垂直居中内容

  • vertical-align 声明只会影响行内元素或者 table-cell 元素
  • 给块级元素设置 vertical-align: middle 后,浏览器会忽略这个声明,故而无效

CSS 中最简单的垂直居中方法是给容器相等的上下内边距,让容器和内容自行决定自己的高度。

垂直居中指南

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

❑ 可以用一个自然高度的容器吗?给容器加上相等的上下内边距让内容居中。

❑ 容器需要指定高度或者避免使用内边距吗?对容器使用 display: table-cellvertical-align: middle

❑ 可以用 Flexbox 吗? 如果不需要支持 IE9,可以用 Flexbox 居中内容。参见第 5 章。

❑ 容器里面的内容只有一行文字吗?设置一个大的行高,让它等于理想的容器高度。这样会让容器高度扩展到能够容纳行高。如果内容不是行内元素,可以设置为 inline-block

❑ 容器和内容的高度都知道吗?将内容绝对定位。参见第 7 章。 ( 只有当前面提到的方法都无效时才推荐这种方式。 )

❑ 不知道内部元素的高度?用绝对定位结合变形 ( transform ) 。参见第 15 章的例子。 ( 还是只有当前面提到的方法都无效时才推荐该方法。 )

负外边距

  • 如果设置左边或顶部的负外边距,元素就会相应地向左或向上移动,导致元素与它前面的元素重叠
  • 如果设置右边或者底部的负外边距,并不会移动元素,而是将它后面的元素拉过来
  • 给元素底部加上负外边距并不等同于给它下面的元素顶部加上负外边距 ( 二者的表现形式不就是如此吗? )

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

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

外边距折叠

  • 折叠外边距的大小等于相邻外边距中的最大值
  • 两个元素不是相邻的兄弟节点也会产生外边距折叠,深层嵌套也会折叠
  • 只有上下外边距会产生折叠,左右外边距不会折叠

当前的设计,<header> 的上下方都出现了 21.44px 的外边距。

  • <h1>,用户代理样式表给它底部设置的外边距为 0.67em ( 21.44px )
  • 它的父元素是 <header>,没有设置任何外边距
  • 因为它们的底部外边距相邻,所以会外边距折叠

从外观效果上看,就像是儿子强行给爸爸顶出了一个外边距,有点老子管不住儿子的意思。

假如我们希望儿子 <h1> 的外边距停留在爸爸 <header> 里面、不逾矩,有两个办法:

  • 头部添加上下内边距,外边距就不会在容器外部折叠
  • Flexbox 布局,这是因为弹性子元素的外边距不会折叠
CSS
header {
  color: #fff;
  background-color: #0072b0;
  border-radius: .5em;
  padding: 1em 1.5em;
}

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

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

容器间的元素间距

容器的内边距和内容的外边距之间的相互作用处理起来很棘手。

HTML
<aside class="sidebar">
  <a href="/twitter" class="button-link">
    follow up on Twitter
  </a>
  <a href="/facebook" class="button-link">
    like us on Facebook
  </a>
</aside>
CSS
.button-link {
  display: block;
  padding: .5em;
  color: #fff;
  background-color: #0090c9;
  text-align: center;
  text-decoration: none;
  text-transform: uppercase;
}

此刻,没有外边距的它们直接上下堆叠在一起。现在有两个选择:分别或同时指定它们的上下外边距,两个按钮之间会发生外边距折叠。

使用相邻的兄弟组合器 ( + ) 选中同一个父元素下紧跟在其他 .button-link 后面的 .button-link 元素。

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

如果侧边栏有更多的内容:

HTML
<aside class="sidebar">
  <a href="/twitter" class="button-link">
    follow up on Twitter
  </a>
  <a href="/facebook" class="button-link">
    like us on Facebook
  </a>
  <a href="/sponsor" class="sponsor-link">
    become a sponsor
  </a>
</aside>

猫头鹰选择器:

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

样式微调整:

CSS
.main {
  background-color: #fff;
  border-radius: .5em;
  width: 70%;
  padding: 1em 1.5em;  /* add padding to main column */
}

.sidebar {
  padding: 1.5em;
  background-color: #fff;
  border-radius: .5em;
  width: 30%;
  margin-left: 1.5em;
  margin-top: 0;  /* overwrite margin-top as set by the lobotomized owl selector */
}

CSS Lobotomized Owl Selector The lobotomized owl selector (* + *) can come in handy to make your CSS stylesheet a bit less verbose and is a handy relative selector for styling. ... the lobotomized owl selects any element that has another element before it. There are more verbose ways to achieve the styling that the lobotomized owl gives us, but this is elegant and clean.

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

总结

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