浏览器排版时,页面上的元素都会落进一个个矩形盒子里。盒子从内到外分为内容区、内边距、边框和外边距;轮廓通常画在边框之外,但不会参与尺寸计算。理解这些层次之后,组件为什么变宽、间距为什么忽大忽小、焦点样式为什么会让页面跳动,就都有了明确的排查入口。

下面我们沿着盒子由内向外看一遍,同时处理几组容易混淆的情况:物理方向与逻辑方向、普通块与行内元素,以及普通边框与图像边框。文中的实验可以直接操作,遇到抽象规则时,不妨先拖动控件看看盒子到底发生了什么变化。
先把“宽度”说清楚。一个元素在页面上占多大位置,并不只由 width 和 height 决定。默认使用 box-sizing: content-box 时,这两个属性描述的只是内容区;padding 和 border 会继续向外增加可见尺寸,margin 再负责拉开它与相邻盒子的距离。改成 border-box 后,声明的宽高会把内容、内边距和边框一起包进去,但仍然不包括外边距。outline 也不在其中,因为它只负责绘制。
.card {
box-sizing: border-box;
width: 320px;
padding: 24px;
border: 4px solid #2563eb;
margin: 20px;
outline: 3px dashed #db2777;
}普通流中的盒子默认不会互相覆盖。使用定位、网格重叠或负外边距后,它们才可能在视觉上叠到一起。还有一个常被忽略的细节:背景默认铺到边框外边缘,所以透过虚线边框的空隙,仍可能看见元素自己的背景。需要改变这个范围时,可以用 background-clip 调整背景的裁剪边界。
调试尺寸前,先确认当前说的是内容宽度、边框盒宽度,还是包含外边距后的外部占位。如果都笼统地叫作“元素宽度”,计算很容易从这里开始出错。
padding 留在内容与边框之间。它接受非负长度或百分比,不会继承,并且可以参与动画。写一个值时四边相同;两个值依次控制“上下、左右”;三个值是“上、左右、下”;四个值则按“上、右、下、左”顺时针展开。需要时,不同位置可以混用不同单位。
.notice { padding: 12px; }
.button { padding: 0.65em 1.25em; }
.panel { padding: 16px 24px 32px; }
.hero { padding: 4vh 6vw 3rem 2rem; }如果方向固定,可以使用 padding-top/right/bottom/left 这组物理长属性。页面需要适应不同书写方向时,逻辑属性通常更省事:
.article {
padding-block: 1rem 2rem; /* 块起点、块终点 */
padding-inline: 1.5rem; /* 行内起点与终点 */
padding-inline-start: 2rem; /* 只覆盖行内起点 */
}四个逻辑长属性分别是 padding-block-start、padding-block-end、padding-inline-start 和 padding-inline-end。轴向缩写可以写一个或两个值;两个值的顺序始终是 start end。它们会根据 writing-mode 与 direction 映射到相应的物理边,而物理属性始终指向上、右、下、左。
百分比 padding 最容易算错:无论写在哪个方向,它都相对包含块的物理宽度计算。假设父容器宽 500px,padding-top: 10% 得到的是 50px,并不是父元素高度的 10%。这条规则既能用来制作等比例留白,也避免了“内容撑高父元素,父元素高度又反过来扩大 padding”的循环依赖。
.fluid-card { padding: 3% 8%; }padding 不能写成负数。如果要把盒子拉向邻居或移出父容器,应考虑负外边距、变换或定位。
到了行内排版里,padding 的表现会有些反直觉。以文本中的 strong 为例,它是普通行内非替换元素。块轴方向的 padding 会画出来,背景也会延伸到这部分区域,但行框高度不会随之增加,因此装饰有可能盖到上下两行。行内轴 padding 会真实占用水平空间,也可能改变换行位置。如果元素横跨多行,默认只在整个元素的开头和结尾绘制行内侧装饰,不会给每个行片段各补一份。
strong {
padding-block: 0.6em; /* 可见,但不撑高行框 */
padding-inline: 1em; /* 推开左右内容 */
background: #fde68a;
}希望每一行的片段都有完整装饰时,可以设置 box-decoration-break: clone,让各片段分别复制 padding、边框和背景。默认值 slice 更接近把一个连续的长盒子切成多段。
图片、嵌入内容这类替换元素遵循另一套表现。padding 会实实在在地包住内容;它们以行内元素出现时,块轴上的 padding、border 和 margin 都会参与行框高度计算。即使图片加载失败或固有尺寸为零,padding 仍可能留下一块可见区域。表单控件通常也属于替换元素,不过原生控件能改到什么程度,还会受到浏览器和操作系统的影响。
一条边框由样式、宽度和颜色共同决定。border-style 控制边框是否存在以及怎么画,border-width 控制厚度,border-color 控制颜色。默认的样式是 none,所以只设置宽度或颜色,页面上仍然看不到边框。样式为 none 或 hidden 时,边框的计算宽度会归零。
可选样式包括 none、hidden、solid、dotted、dashed、double、groove、ridge、inset 和 outset。hidden 与 none 平时都不可见,但在折叠表格处理边框冲突时,两者含义不同。几种立体样式的明暗细节由浏览器决定,不适合拿来做要求像素级一致的设计。
宽度可以写非负长度,也可以用 thin、medium、thick,但不能写百分比。颜色的默认值是 currentColor,会跟随元素的 color。做主题切换时,这种联动往往正合适。
.tag {
color: #2563eb;
border-style: solid dashed dotted double;
border-width: 2px 4px;
border-color: currentColor #f97316 #16a34a #7c3aed;
}这三组四边缩写都遵循 TRBL 顺序和值复制规则。需要单独控制物理边时,可以使用 border-top/right/bottom/left-{style|width|color}。逻辑方向的写法分为两类:一类是轴向缩写 border-block-{style|width|color}、border-inline-{style|width|color};另一类是四个 border-{block|inline}-{start|end}-{style|width|color} 长属性。

border 会同时给四条边设置一组宽度、样式和颜色。border-top/right/bottom/left 以及逻辑侧边缩写 border-block-start/end、border-inline-start/end 只处理其中一条边。单边缩写中的三个值没有固定顺序,但宽度、样式、颜色各自最多只能出现一次。
.card {
border: 2px solid #cbd5e1;
border-inline-start: 6px solid #2563eb;
}
.heading {
border-bottom: solid 3px currentColor;
}缩写会把省略的子属性重置为初始值。也就是说,如果先写长属性,再在后面补一个缩写,前面的定制很可能被重置。实际写样式时,可以先用全局缩写打底,再用长属性或单边缩写覆盖需要调整的部分。
透明边框虽然看不见,却仍然占据空间。利用这一点,可以提前为悬停或选中状态留好位置,避免边框出现时组件突然抖动:
.tab {
border: 3px solid transparent;
}
.tab:hover,
.tab[aria-selected="true"] {
border-color: #2563eb;
}border-top: thin thick solid purple 同时写了两个宽度值,因此整条声明无效。这里的缩写不是四边列表,只允许一份宽度、一份样式和一份颜色。
border-radius 可以接收一至四个长度或百分比。写四个值时,从左上角开始顺时针对应左上、右上、右下、左下;省略值时,复制规则与 padding 相同。长度值给出圆形半径,百分比则分别相对边框盒的宽和高计算,所以经常得到椭圆形圆角。
.card { border-radius: 12px; }
.asymmetric { border-radius: 8px 24px 40px; }
.ellipse { border-radius: 40px / 20px; }
.pill { border-radius: 9999px; }
.circle { aspect-ratio: 1; border-radius: 50斜杠前面是一至四个水平半径,后面是一至四个垂直半径。如果半径大到彼此重叠,浏览器会按比例把它们缩小,因此我们常用超大值快速做出胶囊形。要注意,圆角改变的只是边框与背景的绘制轮廓。布局盒、浮动排斥区和点击布局模型依旧是矩形。
四个物理角长属性是 border-top-left-radius、border-top-right-radius、border-bottom-right-radius 和 border-bottom-left-radius。给单个角写两个半径时,不需要斜杠。逻辑角属性按照“块方向与行内方向的交点”命名,分别是 border-start-start-radius、border-start-end-radius、border-end-start-radius、border-end-end-radius。
相邻边框宽度不同时,圆角处的宽度会平滑过渡;样式或颜色的交界通常更明显,具体画法可能因浏览器而异。
可以把图像边框理解成一张被四条切片线分成九宫格的图片。四个角分别放到边框四角,四条边用来铺边,中间那一块默认丢弃。控制这套过程的属性有五个:
border-image-source:URL 或渐变等图像源。border-image-slice:按上右下左设置切片偏移;数字对位图近似源图像素,百分比相对源图尺寸;fill 保留并绘制中心片。border-image-width:图像边框在边框盒上占多宽,可用长度、百分比、数字倍数或 auto。border-image-outset:把图像边框向边框盒外推,可用长度或相对 border-width 的数字倍数。border-image-repeat:控制边片为 stretch、repeat、round 或 space。
stretch 会拉伸单个边片;repeat 从中间向两侧重复,到了边缘可能截断;round 调整图块尺寸,让边上正好容纳整数份;space 保持图块原有尺寸,把剩余空间均匀分配到图块之间。如果切片值大到相遇或交叉,边片会变空。border-image-width 的相对切线发生交叉时,浏览器则会按比例缩小它们,直到不再交叉。
.certificate {
border: 3px solid #a16207; /* 图片失败时的后备边框 */
border-image-source: url('/ornament.png');
border-image-slice: 30 fill;
border-image-width: 18px;
border-image-outset: 8px;
border-image-repeat: round;
}
/* 顺序:source slice / width / outset repeat */
.certificate {
使用图像边框时,通常仍要声明普通 border。这样既有图片失败时的后备样式,也能保留合理的边框宽度。图像中的透明区域可能透出元素背景;需要控制时,可以调整 outset,或设置 background-clip: padding-box。
outline 最常见的用途是提示焦点。它会被绘制出来,却不占布局空间,因此可能盖到外边距或相邻内容。遇到跨多行的行内片段时,轮廓还可能变成不连续或非矩形的形状。outline 只能整体设置,没有控制单边的属性。
.control:focus-visible {
outline: 3px solid #7c3aed;
outline-offset: 3px;
}outline-style 可以使用 auto、none 和常见的边框样式,但不支持 hidden。outline-width 接受长度或 thin/medium/thick;outline-color 一般显式指定颜色,或者使用 currentColor。三者只有一个缩写,也就是 outline。outline-offset 能把轮廓向外推,写负值时则向内收。
不要为了让界面看起来“更干净”,就给所有焦点元素写 outline: none。确实需要自定义时,应当换成对比度足够、在各种背景上都能辨认的焦点样式。outline 不会改变布局,本来就很适合承担这项工作。
margin 负责盒子外部的间距。它接受长度、百分比和 auto,也允许负值。它的一至四个值沿用 padding 的展开方式和 TRBL 顺序。百分比 margin 无论写在哪个方向,同样相对包含块的物理宽度计算。如果你没有写 margin,标题或段落旁边却仍有空隙,通常是浏览器的用户代理样式预设了 margin,并非该属性的初始值不是零。
物理长属性是 margin-top/right/bottom/left。逻辑长属性包括 margin-block-start/end 与 margin-inline-start/end,对应的两个轴向缩写是 margin-block 和 margin-inline。
.wrapper {
width: min(72rem, 100% - 2rem);
margin-inline: auto;
}
.section {
margin-block: 2rem 4rem;
}在普通块布局中,如果一个块比容器窄,并且左右外边距(或行内起止外边距)都是 auto,剩余空间会被平均分到两侧,元素也就居中了。如果盒子已经占满可用的行内空间,就没有空间可以分配,auto 不会凭空变出间距。在 Flexbox 和 Grid 中,auto margin 也能吸收相应轴上的剩余空间,常用来把某个项目推向一端。

普通流中的块盒相邻时,块轴外边距有可能折叠。两个正外边距相遇,最终间距通常取较大的那个,并不会简单相加。父元素没有 border、padding、行内内容或其他阻隔时,首尾子元素的块轴 margin 甚至可能“穿出”父元素。想阻止特定折叠,可以给父容器增加块轴 padding 或 border,也可以建立新的块格式化上下文,例如设置 display: flow-root,或者改用其他布局方式。
负外边距也会参与折叠计算,不能套用“取较大值”这条简化规则。一个负值会缩短间距,还可能把盒子拉出父容器,甚至造成内容重叠。正值和负值相遇时,可以理解为两者相加;全是负值时,由最负的那个值主导。排查这类问题时,最好直接观察盒子的外边界,不要只凭“两个 margin 应该相加”来猜。
普通行内非替换元素还有自己的规则。它的块轴 margin 不会改变行高,而 margin 本身又是透明的,所以往往完全看不出效果。行内轴 margin 会推开元素开头和结尾处的内容;元素跨行时,默认也只作用在整体首尾,不会作用于每个行片段。负的行内 margin 可能让文字互相重叠。图片等行内替换元素不同,它们的块轴 margin 会影响行框高度。
下面这个实验把四种属性放在同一个组件上。拖动控件时,可以留意三个结果:哪些属性会改变组件自身尺寸,哪些会推开兄弟元素,哪些只负责在表面绘制。
实际写组件时,可以从要解决的问题出发:内容贴边,就加 padding;组件需要一条占位的可见边界,就用 border;键盘焦点这类状态不该改变布局,更适合用 outline;两个盒子之间需要留出外部距离,则交给 margin。页面还要适应多种书写方向时,优先考虑逻辑属性。
none 的 border-style。box-sizing,并把左右 padding 与 border 纳入计算。border-radius 只改变绘制,不改变布局盒。flow-root。:focus-visible 定制。inline-block。border-image-width、padding 与 outset,并准备后备 border。如果要快速排查,可以先沿着盒子从内向外看:padding 是否留出了内容空间,border 是否进入尺寸计算,outline 是否只是绘制标记,margin 是否改变了盒子与外界的距离。接着再检查方向映射、块轴外边距折叠,以及行内非替换元素的特殊绘制规则。这样逐层核对,通常比反复试数值更快找到问题。