Skip to content
This page has been auto-translated and may contain errors.View in English

盒模型

docs.scrimba.com

一旦你开始用 CSS 做间距调整,你就会遇到所有人都遇到的同一个难题:你设置了宽度,加上一些内边距,元素却比你要求的宽。两个段落靠得比数字显示的要近。这都不是 bug。这就是盒模型,一套决定每个元素占用多少空间的规则。一旦你能想象出它的样子,大多数间距上的惊喜就不再是惊喜了。

每个元素都是一个盒子

CSS 把页面上的每个元素都当作一个矩形,哪怕是单个词或一个链接。这个矩形由四层从里到外堆叠而成,这就是**盒模型**:中间是内容,周围是内边距,然后是边框,最外面是外边距。

想象一张挂在墙上的相框照片。照片是内容。照片周围的白色垫子是内边距,是框内的呼吸空间。木制框架本身是边框。这个框架和下一个框架之间的空墙壁是外边距。每个你设置样式的元素都以同样的方式打包,从里到外。

css
.card {
  padding: 16px;              /* 内部空间,围绕内容 */
  border: 2px solid #333;     /* 框架 */
  margin: 24px;               /* 外部空间,推开相邻元素 */
}

浏览器渲染的每个元素都是一个矩形盒子,这个盒子从中心向外测量有四个部分:内容盒(你的文本或图像所在的地方)、内边距、边框和外边距。你在CSS 的工作原理中已经看到过这个概念;盒模型是控制尺寸和间距的部分。

顺序很重要,因为每一层都包裹着前一层。内边距位于内容和边框之间,所以背景色会填充内容和内边距,但在边框外边缘停止。外边距位于边框外部,总是透明的,这就是为什么你不能给外边距上色。

css
.card {
  padding: 16px;              /* 在边框内,共享背景 */
  border: 2px solid #333;
  margin: 24px;              /* 在边框外,始终透明 */
  background: #f4f4f4;       /* 填充内容 + 内边距,在边框处停止 */
}

每个渲染的元素都至少生成一个矩形**盒子**,盒模型是其四个嵌套区域的规范。从里到外:内容盒(保存元素文本、图像或子盒的区域)、内边距盒(内容加内边距,仍然带有元素背景的透明空间)、边框盒(内边距盒加边框,可见的边缘)和外边距盒(边框盒加外边距,最外层的透明区域,将元素与相邻元素分开)。

这种嵌套产生的两个后果会在之后绊倒人。首先,背景绘制内边距盒,而不是外边距盒,所以内边距给你可点击、可上色的内部空间,而外边距给你无法设置样式的空白分隔。其次,你声明的宽度和高度默认情况下不描述整个盒子;它们只描述其中的一个区域,哪个区域取决于 box-sizing,下一节会精确说明。

css
.card {
  padding: 16px;
  border: 2px solid #333;
  margin: 24px;
  background: #f4f4f4;       /* 绘制内容 + 内边距,永不绘制外边距 */
}
Juno每个元素都是一个盒子 每个元素都是一个有四层的盒子,从里到外是:内容、内边距、边框、外边距。相框照片的比喻对我很有帮助:照片、垫子、框架、到下一个框架的间隔。在你脑海中有这个画面,本章的其余部分就会随之而来。
Juno每个元素都是一个盒子 内容、内边距、边框、外边距,从里向外依次包裹。背景填充内容和内边距然后在边框处停止,外边距总是透明的,这就是为什么 margin 不能上色。理清这些嵌套关系,间距规则就不再令人困惑了。
Juno每个元素都是一个盒子 四个嵌套区域:内容盒、内边距盒、边框盒、外边距盒。背景绘制到边框盒然后不再绘制外边距,所以内边距是可上色的内部空间,外边距是空白分隔。而且你声明的 width 只能调整其中一个区域的大小,这就是下一节关于 box-sizing 存在的整个原因。

padding、border 和 margin

三个间距层各有一个作用。**内边距**是内部空间,在内容和边框之间。外边距是外部空间,推开其他元素。边框是它们之间的线。

你可以一次为所有四条边设置一个值,也可以针对单一边:

css
.card {
  padding: 20px;             /* 四条边都是 20px */
  margin-bottom: 32px;       /* 仅底部边 */
}

简写 padding: 20px 是快速说"上、右、下、左都一样"的方法。当你需要一条边与其他边不同时,使用像 margin-bottom 这样的特定属性。

**内边距**是边框内的空间,外边距是边框外的空间,边框是它们之间的边缘。要记住的规则是外边距推开相邻元素,内边距增加元素自身的内部。如果背景看起来靠近文本太紧凑,那是内边距的事。如果两个元素靠得太近,那是外边距的事。

每个都采用从顶部顺时针填充边的简写。一个值应用于所有四条边,两个值分别设置垂直和水平,四个值按上、右、下、左顺序排列:

css
.hero {
  padding: 40px 24px;        /* 上下 40px,左右 24px */
  margin: 0 auto;            /* 垂直外边距为 0,左右自动居中 */
}

.card {
  padding-left: 16px;        /* 仅一条边不同时按边设置 */
}

内边距、边框和外边距是三个独立的间距控制,行为不同,不是同一个间隔的三个名称。内边距是边框内的透明区域,仍然带有背景且仍然响应点击。外边距是边框外的透明区域,将元素与兄弟元素分开,不带背景。边框是绘制的边缘,也占用等于其宽度的布局空间。

这三个都接受顺时针简写(一个、两个、三个或四个值扩展到上/右/下/左),都有按边的长形式。两个行为值得精确把握。外边距可以是负数,这会拉动元素靠近或覆盖相邻元素,是合法的布局工具,而内边距不能低于零。而且 margin: 0 auto 仅当元素有明确宽度时才能使块水平居中,因为 auto 通过分割剩余水平空间来解析,如果盒子已经填充其容器就没有剩余空间可分割。

css
.hero {
  padding: 40px 24px;        /* 顺时针对:垂直、水平 */
  margin: 0 auto;            /* 仅在 .hero 设置宽度时居中 */
  border-bottom: 1px solid #ddd;  /* 边框长形式,一条边,占用 1px 布局 */
}

.card {
  margin-top: -8px;          /* 负外边距将卡片向上拉动 */
}
Junopadding、border 和 margin 内边距是边框内的空间,外边距是边框外的空间,边框是其间的线。当文本在盒子内看起来紧凑时使用 padding,当两个盒子靠得太近时使用 margin。一个像 padding: 20px 这样的值覆盖所有四条边,或在只有一条边需要改变时命名一条边如 margin-bottom
Junopadding、border 和 margin 内边距增加元素自身的内部,外边距推开相邻元素,知道你想要哪个省去很多猜测。简写从顶部顺时针运行:一个值适用所有边,两个值分别适用垂直和水平。当背景与文本贴得太紧时你需要的是内边距,不是外边距。
Junopadding、border 和 margin 内边距带背景和点击事件,外边距是透明分隔,边框占用等于其宽度的真实布局空间。外边距可以是负数来拉动元素靠近,内边距不能。而且 margin: 0 auto 仅能使有明确宽度的块居中,因为 auto 否则没有剩余空间可分割。

box-sizing:content-box vs border-box

这是让几乎所有人吃惊的地方。你设置卡片为 width: 300px,加上一些内边距,卡片在页面上的宽度就超过了 300 像素。你没做错。默认情况下,width 只测量中间的内容,任何内边距和边框都被加到上面。

有一个单行修复让 width 按你期望的方式表现。设置 box-sizing: border-box,现在你输入的宽度是盒子的完整宽度,包括内边距和边框:

css
.card {
  box-sizing: border-box;    /* width 现在包含内边距和边框 */
  width: 300px;
  padding: 20px;             /* 卡片保持 300px 宽,不是 340px */
}

默认情况下每个元素都使用 box-sizing: content-box,这意味着你设置的 width 只应用于内容盒。然后内边距和边框被加到其外面,所以你在页面上实际看到的盒子比你输入的数字更宽。这是百次布局崩溃背后的经典尺寸陷阱。

做一次数学运算就清楚了。使用 content-box,一个 300px 宽度加上每边 20px 内边距加上每边 2px 边框会渲染为 300 + 40 + 4 = 344px。改为**border-box**,width 现在意味着边框盒:内边距和边框从内部减去,所以元素保持正好 300px,内容缩小以适应。

css
.card {
  box-sizing: content-box;   /* 默认值 */
  width: 300px;
  padding: 20px;
  border: 2px solid #333;
  /* 实际渲染宽度:300 + 20 + 20 + 2 + 2 = 344px */
}

因为这几乎总是你想要的,标准做法是用通用选择器全局设置它,这样每个元素都加入:

css
* {
  box-sizing: border-box;    /* 一条规则,处处可预测的尺寸 */
}

box-sizing 决定你的 widthheight 测量哪个区域,它有两个值。初始值是 content-boxwidth 设置内容盒,内边距和边框被布局在该宽度外面,所以边框盒,即页面上的实际占地面积,是 width + padding-left + padding-right + border-left + border-right。替代方案是**border-box**:width 直接设置边框盒,浏览器从内部减去内边距和边框,缩小内容盒以适应剩余空间。

具体的算数值得精确说明,因为它是这个属性存在的整个原因。给定 width: 300px; padding: 20px; border: 2px

css
/* content-box(默认):内边距和边框加在顶部 */
.card {
  box-sizing: content-box;
  width: 300px;
  padding: 20px;
  border: 2px solid #333;
  /* 边框盒 = 300 + 40 + 4 = 344px;内容盒 = 300px */
}

/* border-box:内边距和边框蚕食声明的宽度 */
.card {
  box-sizing: border-box;
  width: 300px;
  padding: 20px;
  border: 2px solid #333;
  /* 边框盒 = 300px;内容盒 = 300 - 40 - 4 = 256px */
}

推荐 border-box:它使尺寸可组合。原因很实际:使用 border-box,一组声明为 width: 25% 的列无论你加什么内边距都保持容器的四分之一,因为内边距不再改变外部尺寸。这种可预测性就是为什么接近通用的重置将其应用于所有内容,通常包括伪元素,这样没有什么会悄悄回到 content-box

css
*, *::before, *::after {
  box-sizing: border-box;
}

一个精确说明:这只控制 widthheight。外边距总是在边框盒外,无论 box-sizing 如何,所以它们不会计入声明的尺寸,这直接关联到下面的外边距折叠。如果你需要单位来可靠地调整这些盒子的大小,尺寸单位涵盖百分比、rem 和视口单位如何解析。

Junobox-sizing:content-box vs border-box 默认情况下,你设置的 width 只测量中间的内容,所以内边距和边框使盒子比你输入的数字更大。设置 box-sizing: border-box 修复了这个:宽度变成完整的盒子,包括内边距和边框。这个绊倒了我很久,所以早点记住它值得。
Junobox-sizing:content-box vs border-box 默认的 content-box 意味着 width 仅调整内容的大小,所以内边距和边框把真实的盒子推得比你要求的更宽。一个有 20px 内边距和 2px 边框的 300px 卡片实际渲染为 344px。改为 border-box,宽度意味着整个盒子,这就是为什么在 * 上全局设置它是大多数样式表的标准首行。
Junobox-sizing:content-box vs border-boxcontent-box 将内边距和边框加到你的宽度之外,所以 300px 盒子变成 344pxborder-box 从内部减去它们并保持在 300px。选择 border-box 是因为百分比宽度在内边距不再改变外部尺寸时保持可组合。将其应用于 * 加上伪元素,这样没有什么会悄悄回到默认值。

外边距折叠

还有一个惊喜,它是关于外边距堆叠。把两个段落放在彼此上面,给第一个 24px 的底部外边距,给第二个 16px 的顶部外边距,你可能期望它们之间有 40px 的间隔。你实际上得到 24px

这被称为**外边距折叠**。当两个垂直外边距相遇时,浏览器不会将它们相加。它保留较大的两个,丢弃较小的。所以较大的外边距赢,间隔是 24px,不是 40px。它仅与上下外边距发生,永远不与内边距发生。

css
.intro {
  margin-bottom: 24px;
}
.details {
  margin-top: 16px;
  /* 它们之间的间隔是 24px,较大的外边距,不是 40px */
}

当一个块的底部外边距遇到下一个块的顶部外边距时,这两个不会相加。它们折叠成一个等于两者中较大值的单个外边距。给一个段落 margin-bottom: 24px,下一个 margin-top: 16px,它们之间的空间是 24px,不是 40px。这是**外边距折叠**,它只影响垂直外边距。

了解它的原因是它改变了你推理间距的方式。如果一个间隔比两个数字建议的要小,你可能在看一个折叠,向较小的外边距添加内容在它超过较大的外边距之前什么都不做。控制垂直韵律的干净方法是仅在一个方向设置外边距,例如每个块的 margin-bottom 而没有 margin-top,所以折叠没有什么要解析。

css
.article > * {
  margin-top: 0;
  margin-bottom: 24px;       /* 一个方向,所以间隔可预测 */
}

**外边距折叠**是相邻垂直外边距在正常流中合并为一个的规则,其大小是两者中较大的(或最正的,负数通过对最大正数和最负数求和来处理)。正常流是块盒的默认从上到下堆叠,在任何 flex、grid 或 float 改变它之前。给 .intro 一个 24px 底部外边距,.details 一个 16px 顶部外边距,间隔是 24px,因为浏览器将两者都解析为一个折叠的外边距。

精确部分是知道什么确实折叠,什么不折叠,因为逃生是你停止惊喜的方式:

  • 它仅影响垂直外边距。水平外边距永不折叠;两个并排的元素总是加上它们面向的外边距。
  • 两个外边距之间的任何东西都会停止折叠:边框、任何内边距,或父元素内容与其自身外边距之间的内容。
  • 外边距不会跨过格式化上下文边界折叠。一个flex 或 grid 容器为其子元素建立一个,所以其内的项永不折叠其外边距,这是现代布局感觉比浮动时代更可预测的原因之一。
  • 当没有东西将它们分开时,父元素和其第一个或最后一个子元素可以穿过彼此折叠,所以子元素的顶部外边距可能意外出现在父元素外。向父元素添加 padding-top 或边框会停止它。
css
/* 折叠发生:正常流中的相邻兄弟 */
.intro { margin-bottom: 24px; }
.details { margin-top: 16px; }   /* 间隔解析为 24px */

/* 折叠停止:外边距之间的边框 */
.section {
  border-top: 1px solid #eee;    /* 现在两个外边距都应用,没有合并 */
}
Juno外边距折叠 当底部外边距遇到顶部外边距时,浏览器保留较大的一个并丢弃较小的,而不是将它们相加。所以 24px 下面遇见 16px 上面给出 24px 的间隔,不是 40px。它仅与上下外边距发生,第一次它会让每个人吃惊。
Juno外边距折叠 块之间的垂直外边距折叠到两者中的较大值,从不是总和,所以一个间隔可能比数字建议的要小。当这发生时,增加较小的外边距直到它超过较大的才会起作用。仅在一个方向设置外边距,如每个块上的 margin-bottom,避免了整个问题。
Juno外边距折叠 相邻的垂直外边距在正常流中合并到较大值,水平外边距永不。边框或内边距在它们之间会停止折叠,flex 和 grid 容器完全停止它因为它们创建新的格式化上下文。注意父子第一个的情况,子元素的顶部外边距逃脱父元素直到你向父元素添加 padding-top 或边框来保持它。