CSS 盒模型
问题
什么是 CSS 盒模型?标准盒模型和 IE 盒模型有什么区别?margin 塌陷和合并分别是什么?
答案
盒模型概述
CSS 盒模型(Box Model)是 CSS 布局的基础概念。每个 HTML 元素都可以看作一个矩形盒子,由内到外依次由四部分组成:
┌─────────────────────────────────────┐
│ margin │
│ ┌───────────────────────────────┐ │
│ │ border │ │
│ │ ┌───────────────────────┐ │ │
│ │ │ padding │ │ │
│ │ │ ┌───────────────┐ │ │ │
│ │ │ │ content │ │ │ │
│ │ │ └───────────────┘ │ │ │
│ │ └───────────────────────┘ │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
| 区域 | 说明 |
|---|---|
| content | 内容区域,放置文本、图片等实际内容 |
| padding | 内边距,内容与边框之间的空间 |
| border | 边框,包围内边距和内容的线条 |
| margin | 外边距,元素与其他元素之间的空间 |
标准盒模型 vs IE 盒模型
两种盒模型的核心区别在于 width 和 height 的计算范围不同:
| 特性 | 标准盒模型 (content-box) | IE 盒模型 (border-box) |
|---|---|---|
width 包含 | 仅 content | content + padding + border |
height 包含 | 仅 content | content + padding + border |
| 实际占用宽度 | width + padding + border | width(padding、border 被包含在内) |
| 默认行为 | CSS 标准默认值 | IE 5.5 怪异模式默认值 |
标准盒模型(content-box)
.box {
box-sizing: content-box; /* 默认值 */
width: 200px;
padding: 20px;
border: 5px solid #333;
}
/* 实际占用宽度 = 200 + 20*2 + 5*2 = 250px */
IE 盒模型(border-box)
.box {
box-sizing: border-box;
width: 200px;
padding: 20px;
border: 5px solid #333;
}
/* 实际占用宽度 = 200px(content 区域被压缩为 150px) */
现代开发中通常使用全局 border-box,它更符合直觉 —— 设置 width: 200px 就是 200px:
*,
*::before,
*::after {
box-sizing: border-box;
}
几乎所有 CSS 框架(Tailwind CSS、Bootstrap)都采用此方案。
margin 合并(Margin Collapsing)
当两个垂直方向的 margin 相邻时,它们不会叠加,而是取较大值合并为一个。这种现象仅发生在块级元素的垂直方向。
三种触发场景
1. 相邻兄弟元素
.box-a {
margin-bottom: 30px;
}
.box-b {
margin-top: 20px;
}
/* A 和 B 之间的间距 = max(30, 20) = 30px,而不是 50px */
2. 父元素与第一个/最后一个子元素
.parent {
margin-top: 0;
}
.child:first-child {
margin-top: 20px;
}
/* 子元素的 margin-top 会"穿透"父元素,表现为父元素有 20px 的 margin-top */
3. 空块级元素
.empty {
margin-top: 20px;
margin-bottom: 30px;
}
/* 如果该元素没有 border、padding、height、min-height、内容,
则自身的上下 margin 合并,最终占 30px */
margin 合并的计算规则
| 情况 | 结果 |
|---|---|
| 两个正值 | 取较大值 |
| 一正一负 | 相加(正 + 负) |
| 两个负值 | 取绝对值较大的负值 |
/* 示例:一正一负 */
.a { margin-bottom: 30px; }
.b { margin-top: -10px; }
/* 间距 = 30 + (-10) = 20px */
阻止 margin 合并的方法
| 方法 | 原理 | 适用场景 |
|---|---|---|
| 创建 BFC | BFC 内部与外部互不影响 | 父子 margin 合并 |
设置 border 或 padding | 隔开相邻 margin | 父子 margin 合并 |
| 使用 Flex/Grid 布局 | Flex/Grid 的子元素不发生合并 | 所有场景 |
display: inline-block | 行内块不参与 margin 合并 | 兄弟元素合并 |
overflow: hidden/auto | 创建 BFC | 父子 margin 合并 |
/* 方法一:父元素设置 overflow */
.parent {
overflow: hidden; /* 创建 BFC */
}
/* 方法二:父元素设置 border/padding */
.parent {
padding-top: 1px; /* 即使 1px 也能阻止合并 */
}
/* 或 */
.parent {
border-top: 1px solid transparent;
}
/* 方法三:使用 Flex 布局 */
.parent {
display: flex;
flex-direction: column;
}
margin 塌陷
"margin 塌陷"通常特指父子元素的 margin 合并现象:子元素设置 margin-top 时,效果表现在父元素上而非子元素自身,就像子元素的 margin "塌陷"到了父元素身上。
<div class="parent">
<div class="child">Hello</div>
</div>
<style>
.parent {
background: #eee;
/* 没有 border、padding、overflow 等属性 */
}
.child {
margin-top: 50px;
/* 期望:child 距离 parent 顶部 50px */
/* 实际:parent 整体下移 50px(margin 穿透了父元素) */
}
</style>
- margin 合并:是 CSS 规范定义的正常行为,涵盖所有垂直方向 margin 重叠的情况
- margin 塌陷:是开发者对"父子 margin 合并"这一特定场景的通俗叫法,属于 margin 合并的子集
面试中两个概念经常混用,理解本质即可。
获取盒模型的尺寸
在 JavaScript 中获取元素尺寸的常用方式:
const el = document.querySelector('.box') as HTMLElement;
// 1. 内容区 + padding + border(视觉可见区域)
console.log(el.offsetWidth, el.offsetHeight); // 包含 border
// 2. 内容区 + padding(不含 border 和滚动条)
console.log(el.clientWidth, el.clientHeight);
// 3. 实际内容的完整尺寸(包含溢出部分)
console.log(el.scrollWidth, el.scrollHeight);
// 4. 精确尺寸(可能包含小数,包含 transform 后的尺寸)
const rect = el.getBoundingClientRect();
console.log(rect.width, rect.height); // 包含 border
| 属性 | 包含的区域 | 是否整数 | 是否受 transform 影响 |
|---|---|---|---|
offsetWidth/Height | content + padding + border + scrollbar | 是(四舍五入) | 否 |
clientWidth/Height | content + padding(不含 scrollbar) | 是(四舍五入) | 否 |
scrollWidth/Height | 实际内容(含溢出) | 是 | 否 |
getBoundingClientRect() | content + padding + border | 否(可能小数) | 是 |
getComputedStyle 获取计算后样式
const el = document.querySelector('.box') as HTMLElement;
const style = window.getComputedStyle(el);
console.log(style.width); // "200px"
console.log(style.paddingTop); // "20px"
console.log(style.marginTop); // "10px"
console.log(style.boxSizing); // "border-box"
getComputedStyle 返回的 width 总是指 content 区域的宽度,无论 box-sizing 是什么值。如果是 border-box,返回的 width 值 = 设置的 width - padding - border。
常见面试问题
Q1: 介绍一下 CSS 盒模型
答案:
CSS 盒模型描述了元素在页面中占据空间的方式,由四部分组成(从内到外):content(内容区)、padding(内边距)、border(边框)、margin(外边距)。
有两种盒模型:
- 标准盒模型(
box-sizing: content-box):width/height只包含 content,实际宽度 = width + padding + border - IE 盒模型(
box-sizing: border-box):width/height包含 content + padding + border
现代开发推荐使用 border-box,因为更直观。
Q2: box-sizing 有哪些值?分别什么含义?
答案:
| 值 | 含义 |
|---|---|
content-box(默认) | width/height 只包含内容区,padding 和 border 额外计算 |
border-box | width/height 包含内容区 + padding + border |
/* 两个 .box 视觉宽度都是 200px */
.box-standard {
box-sizing: content-box;
width: 160px; /* 160 + 15*2 + 5*2 = 200px */
padding: 15px;
border: 5px solid;
}
.box-border {
box-sizing: border-box;
width: 200px; /* 直接就是 200px */
padding: 15px;
border: 5px solid;
}
Q3: 什么是 margin 合并?怎么解决?
答案:
垂直方向上相邻的 margin 会发生合并,取较大值而非叠加。有三种场景:
- 相邻兄弟元素的上下 margin
- 父元素与第一个/最后一个子元素的 margin
- 空块元素自身的上下 margin
解决方式:
- 创建 BFC(
overflow: hidden/display: flow-root) - 设置
border或padding隔开 - 使用 Flex/Grid 布局
- 改为
inline-block
Q4: offsetWidth、clientWidth、scrollWidth 的区别?
答案:
┌───────────────────────────┐
offsetWidth → │ border + padding + content + scrollbar │
└───────────────────────────┘
┌───────────────────────────┐
clientWidth → │ padding + content(不含 scrollbar)│
└───────────────────────────┘
┌───────────────────────────┐
scrollWidth → │ 实际内容区(含溢出不可见部分)│
└───────────────────────────┘
offsetWidth= border + padding + content + scrollbar(元素完整视觉宽度)clientWidth= padding + content - scrollbar(元素内部可用宽度)scrollWidth= 实际内容宽度,包括溢出(overflow)的部分getBoundingClientRect().width与offsetWidth类似,但返回浮点数且受transform影响
Q5: 为什么推荐全局使用 box-sizing: border-box?
答案:
- 更直观:
width: 300px就是 300px,不需要手动减去 padding 和 border - 布局更方便:百分比宽度 + padding 不会溢出父容器
- 一致性:
<input>、<textarea>等表单元素浏览器默认值不一致,统一设置可避免 bug - 行业标准:所有主流 CSS 框架(Bootstrap、Tailwind、Ant Design)都采用此方案
/* 经典重置 */
*, *::before, *::after {
box-sizing: border-box;
}
Q6: margin 设置百分比是相对于谁的?
答案:
margin 的百分比值始终相对于包含块的宽度(width),包括 margin-top 和 margin-bottom。
.parent {
width: 400px;
height: 800px;
}
.child {
margin-top: 10%; /* 40px(400 * 10%),而不是 800 * 10% = 80px */
margin-left: 10%; /* 40px */
}
这是因为 CSS 规范为了保证在水平和垂直方向上 margin 的百分比计算一致性,统一使用包含块的宽度作为参考。同理,padding 的百分比也是相对于包含块的宽度。
Q7: 如何用 padding 实现一个固定宽高比的容器?
答案:
利用 padding 百分比相对于父元素宽度计算的特性:
.aspect-ratio-box {
width: 100%;
padding-top: 56.25%; /* 16:9 = 9/16 = 0.5625 */
position: relative;
height: 0;
}
.aspect-ratio-box > * {
position: absolute;
inset: 0;
}
.aspect-ratio-box {
aspect-ratio: 16 / 9;
width: 100%;
}
Q8: outline 和 border 有什么区别?
答案:
| 特性 | border | outline |
|---|---|---|
| 是否占据空间 | 是,参与盒模型计算 | 否,不影响布局 |
| 是否可以单独设置某一边 | 可以(border-top 等) | 不可以 |
| 是否可以设置圆角 | 是(border-radius) | 是(outline + border-radius) |
| 是否影响元素尺寸 | 是 | 否 |
| 典型用途 | 装饰性边框 | 焦点指示(:focus) |
.btn:focus-visible {
outline: 2px solid blue;
outline-offset: 2px; /* outline 可以设置偏移 */
}
Q9: box-shadow 会影响盒模型布局吗?
答案:
不会。box-shadow 不占据空间,不影响布局。它只是视觉上的装饰效果,类似于 outline。如果需要阴影占据空间,可以通过等量的 margin 来补偿。
.card {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
/* 阴影不占空间,相邻元素不会被推开 */
}
Q10: 行内元素的盒模型有什么特殊之处?
答案:
行内元素(display: inline,如 <span>、<a>)的盒模型与块级元素不同:
| 特性 | 行内元素 | 块级元素 |
|---|---|---|
width/height | 无效 | 有效 |
水平方向 padding | 有效 | 有效 |
垂直方向 padding | 有效但不影响布局(只影响背景区域) | 有效且影响布局 |
水平方向 margin | 有效 | 有效 |
垂直方向 margin | 无效 | 有效 |
border | 有效但垂直方向不影响布局 | 有效 |
span {
padding: 20px; /* 水平方向正常,垂直方向不推开相邻行 */
margin: 20px; /* 水平方向正常,垂直方向无效 */
border: 2px solid; /* 同 padding */
}
要让行内元素的垂直方向属性生效,需改为 display: inline-block 或 display: block。