CSS 核心概念
CSS(Cascading Style Sheets,层叠样式表)的大部分“玄学”问题,都能归结到几个核心机制:样式冲突时谁生效(层叠与优先级)、值从哪来(继承)、元素占多大地方(盒模型)、元素放在哪(格式化上下文、定位、Flex/Grid)、谁盖住谁(层叠上下文)。这篇逐个讲清这些机制,读完遇到“样式不生效”“z-index 没用”“margin 合并了”这类问题时能定位到原因。
层叠:样式冲突时谁生效
Section titled “层叠:样式冲突时谁生效”同一个元素的同一个属性可能被多条规则声明,浏览器按以下顺序逐级比较,前一级分出胜负就不再往后看:
- 来源与重要性
- 层叠层(
@layer) - 优先级(特异性)
- 出现顺序:后声明的覆盖先声明的
来源与重要性
Section titled “来源与重要性”样式有三个来源:浏览器默认样式(user-agent)、用户样式(用户在浏览器里设置的)、作者样式(开发者写的)。加上 !important 后顺序会反转,从低到高:
| 优先级 | 来源 |
|---|---|
| 1(最低) | 浏览器默认样式 |
| 2 | 用户样式 |
| 3 | 作者样式 |
| 4 | CSS 动画(@keyframes) |
| 5 | 作者样式中的 !important |
| 6 | 用户样式中的 !important |
| 7 | 浏览器默认样式中的 !important |
| 8(最高) | CSS 过渡(transition) |
反转的设计是为了让用户的 !important(比如无障碍需要的大字号)能压过网页作者。日常开发中只需记住:!important 胜过普通声明,两条都带 !important 时再按后面的规则比较。
@layer 允许把样式分组并显式声明层的顺序,层的顺序比优先级更先比较:
@layer reset, base, components;
@layer reset { button { padding: 0; }}@layer components { .btn { padding: 8px 16px; }}后声明的层胜出,与选择器优先级无关;没有放进任何层的样式比所有层都优先。这让第三方样式、重置样式可以放进低优先级的层,业务代码不必靠堆叠选择器去覆盖它们。(!important 的层顺序是反过来的,这里不展开。)
优先级(特异性)计算
Section titled “优先级(特异性)计算”优先级是一个三元组 (A, B, C),从左往右逐位比较,高位大就胜出,不会进位:
| 位 | 计数 |
|---|---|
| A | ID 选择器 #id |
| B | 类 .cls、属性选择器 [type="text"]、伪类 :hover |
| C | 元素选择器 div、伪元素 ::before |
不计入优先级的:通用选择器 *、组合符(空格、>、+、~)、:where()。
| 选择器 | 优先级 |
|---|---|
* |
(0, 0, 0) |
li |
(0, 0, 1) |
ul li::before |
(0, 0, 3) |
.nav a:hover |
(0, 2, 1) |
input[type="text"] |
(0, 1, 1) |
#app .item |
(1, 1, 0) |
:is(#a, .b) p |
(1, 0, 1) |
:where(#a, .b) p |
(0, 0, 1) |
几条规则:
- 一个 ID 胜过任意多个类,因为是逐位比较而不是加总。
- 选择器列表
a, .b不是一个选择器,每一项单独计算,各自匹配各自的元素。 :is()、:not()、:has()本身不计数,取参数里优先级最高的那一项;:where()恒为 0。- 行内
style属性的优先级高于任何选择器;!important又能压过行内样式。
没有被任何规则声明的属性,取值来源分两种:
- 可继承属性取父元素的计算值。主要是文字相关的:
color、font-*、line-height、text-align、letter-spacing、white-space、visibility、cursor、list-style等。 - 不可继承属性取初始值。主要是盒子和布局相关的:
width/height、margin、padding、border、background、position、display、overflow、z-index等。
这样设计是合理的:给 <body> 设置字体,整页文字都跟着变;但给容器设置边框,不应该让每个子元素都带上边框。
可以用全局关键字显式控制,所有属性都支持:
| 关键字 | 含义 |
|---|---|
inherit |
强制取父元素的值 |
initial |
取规范定义的初始值(不是浏览器默认样式) |
unset |
可继承属性等于 inherit,否则等于 initial |
revert |
回退到浏览器默认样式(或用户样式) |
revert-layer |
回退到上一个层叠层的值 |
initial 常被误解:display 的初始值是 inline,所以给 <div> 写 display: initial 得到的是行内元素,而不是块级。想恢复浏览器默认表现应该用 revert。
盒模型与 box-sizing
Section titled “盒模型与 box-sizing”每个元素都生成一个矩形盒子,从内到外依次是内容(content)、内边距(padding)、边框(border)、外边距(margin)。
box-sizing 决定 width / height 指的是哪一层:
| 值 | width 包含 |
元素实际占用宽度 |
|---|---|---|
content-box(默认) |
只有内容 | width + padding + border |
border-box |
内容 + padding + border | width |
.box { width: 200px; padding: 20px; border: 1px solid;}/* content-box:实际占 242px;border-box:实际占 200px,内容区 158px */border-box 更符合直觉,现在几乎所有项目都会全局设置:
*, *::before, *::after { box-sizing: border-box;}普通文档流中,块级元素垂直方向相邻的外边距会合并(collapse),取较大值而不是相加:
- 相邻兄弟:上面元素的
margin-bottom和下面元素的margin-top合并。 - 父子:父元素没有边框、内边距、行内内容或 BFC 隔开时,第一个子元素的
margin-top会“穿透”到父元素外面。 - 空块:自身的上下外边距合并。
水平方向的外边距、Flex 和 Grid 子项的外边距都不会合并。
BFC:块级格式化上下文
Section titled “BFC:块级格式化上下文”BFC(Block Formatting Context)是一块独立的布局区域:里面的元素怎么排布不影响外面,外面也不影响里面。
以下情况会创建新的 BFC(常见的几种):
- 根元素
<html> - 浮动元素(
float不为none) - 绝对定位元素(
position: absolute或fixed) display: inline-block、table-cell、flow-rootoverflow不为visible和clip的块元素- Flex 和 Grid 容器的直接子项
BFC 的效果:
- 包含内部浮动:计算高度时会把浮动子元素算进去,解决“子元素浮动后父元素高度塌陷”。
- 阻止与子元素的外边距合并:BFC 容器和它的子元素之间不发生 margin 穿透。
- 不与浮动元素重叠:BFC 区域不会和旁边的浮动元素重叠,可用于两栏布局。
过去常用 overflow: hidden 创建 BFC,但它会裁剪溢出内容。现在有专门的写法,没有副作用:
.container { display: flow-root;}浮动布局如今已基本被 Flex 和 Grid 取代,BFC 更多出现在解释“为什么外边距没合并”“为什么高度包住了浮动”这类问题时。
position |
是否脱离文档流 | 偏移参照 |
|---|---|---|
static(默认) |
否 | 不响应 top/left 等 |
relative |
否,原位置保留 | 自身原来的位置 |
absolute |
是 | 最近的非 static 定位祖先(包含块) |
fixed |
是 | 视口 |
sticky |
否 | 滚动到阈值前像 relative,之后“粘”在最近的滚动容器上 |
几个常见坑:
absolute找的是最近的定位祖先;没有就相对初始包含块(大致是首屏视口大小的区域)。常见写法是父元素position: relative、子元素position: absolute。- 祖先元素设置了
transform、filter、perspective等属性时,会成为fixed和absolute后代的包含块。这时fixed元素不再相对视口定位,弹窗“跟着容器跑”多半是这个原因。 sticky需要设置top等阈值才生效;它相对最近的滚动容器粘住。祖先设置overflow: hidden(或auto、scroll)后会成为它的滚动容器,如果这个祖先本身并不滚动,sticky 看起来就像失效了;只想裁剪溢出内容时可以改用overflow: clip,它不会产生滚动容器;粘住的范围也不会超出父元素。
Flex 布局要点
Section titled “Flex 布局要点”Flex 是一维布局:沿一条主轴排列子项,再处理交叉轴的对齐。
.row { display: flex; justify-content: space-between; /* 主轴对齐 */ align-items: center; /* 交叉轴对齐 */ gap: 12px; /* 子项间距 */}容器上的常用属性:flex-direction(主轴方向)、flex-wrap(是否换行)、justify-content、align-items、align-content(多行时行的分布)、gap。
子项上最关键的是 flex 简写,它是 flex-grow、flex-shrink、flex-basis 的组合:
| 写法 | 等价于 | 效果 |
|---|---|---|
flex: 1 |
1 1 0% |
基准尺寸从 0 起算,按比例平分空间(受下面的 min-width 陷阱影响) |
flex: auto |
1 1 auto |
在内容宽度基础上分配剩余空间 |
flex: none |
0 0 auto |
不伸不缩,保持内容宽度 |
flex: 0 0 200px |
固定 200px |
一个高频坑:Flex 子项的 min-width 默认值是 auto,即不会收缩到比内容更窄。子项里有长文本或长单词时,即使写了 flex: 1 也会把容器撑破,文本省略号也不生效。解决方式是给子项加 min-width: 0(纵向主轴对应 min-height: 0)。
Grid 布局要点
Section titled “Grid 布局要点”Grid 是二维布局:同时定义行和列,子项放进网格单元。
.layout { display: grid; grid-template-columns: 200px 1fr; grid-template-rows: auto 1fr auto; grid-template-areas: "header header" "aside main" "footer footer"; gap: 16px; min-height: 100vh;}.header { grid-area: header; }.aside { grid-area: aside; }.main { grid-area: main; }.footer { grid-area: footer; }fr表示剩余空间的份数,1fr 2fr按 1:2 分配。repeat(auto-fill, minmax(200px, 1fr))能在不写媒体查询的情况下实现自适应列数的卡片网格:每列至少 200px,放不下就换行。auto-fit和auto-fill的区别在于项目较少时,auto-fit会把空列折叠掉、让现有项目拉伸占满。- 子项可以用
grid-column: 1 / 3或grid-column: span 2跨列。 subgrid让嵌套网格沿用父网格的轨道,适合需要跨卡片对齐内容的场景。
选择原则:沿一个方向排列、尺寸由内容决定,用 Flex;需要行列同时对齐的整体布局,用 Grid。两者经常嵌套使用。
层叠上下文与 z-index
Section titled “层叠上下文与 z-index”z-index 只在同一个层叠上下文里比较。每个层叠上下文是一个整体,内部元素的 z-index 再大,也越不过父级上下文在外层的位置。
以下情况会创建层叠上下文(常见的几种):
- 根元素
<html> position为relative/absolute且z-index不为autoposition: fixed或sticky- Flex、Grid 子项且
z-index不为auto opacity小于 1transform、filter、perspective、clip-path、mask不为noneisolation: isolatewill-change指定了上述属性mix-blend-mode不为normal
一个典型问题:
<div class="card" style="transform: translateZ(0)"> <div class="dropdown" style="position: absolute; z-index: 9999">下拉框</div></div><div class="next-card" style="position: relative; z-index: 1">下一张卡片</div>.card 因为 transform 创建了层叠上下文,且自身 z-index 相当于 0,于是整个 .card(包括里面的下拉框)都排在 z-index: 1 的 .next-card 下面,下拉框写 9999 也没用。解决方法是去掉父级创建上下文的属性、提高父级的层级,或把下拉框挂到 <body> 下(Portal)。
同一层叠上下文内,从下到上的绘制顺序大致是:上下文根的背景和边框 → z-index 为负的定位元素 → 普通块级元素 → 浮动元素 → 行内内容 → z-index: auto 或 0 的定位元素 → z-index 为正的定位元素。
isolation: isolate 是专门用来创建层叠上下文且没有其他副作用的属性,适合给组件根节点加上,把组件内部的 z-index 关在组件里面。
CSS 变量
Section titled “CSS 变量”CSS 自定义属性以 -- 开头,用 var() 读取:
:root { --color-primary: #1677ff; --space: 8px;}
.btn { color: var(--color-primary); padding: var(--space) calc(var(--space) * 2); border-color: var(--btn-border, currentColor); /* 第二个参数是回退值 */}
.dark { --color-primary: #4096ff; /* 在子树里重新赋值,局部生效 */}它和 Sass 等预处理器变量的根本区别:
| 方面 | CSS 变量 | 预处理器变量 |
|---|---|---|
| 生效时机 | 运行时,浏览器计算样式时解析 | 编译时替换成固定值 |
| 作用域 | 跟随 DOM 树,可继承,可在子树内覆盖 | 跟随源码的词法作用域 |
| 能否被 JS 修改 | 能:el.style.setProperty('--x', '1') |
不能 |
| 能否用于媒体查询条件 | 不能 | 能 |
所以主题切换、暗色模式这类“运行时改一批值”的需求适合用 CSS 变量。变量值引用无效时,属性会变成“计算时无效”,表现为取继承值或初始值,而不是回退到前一条声明,这点和普通的无效声明不同。
需要给变量指定类型(比如让渐变颜色可以过渡动画)时,可以用 @property 注册:
@property --angle { syntax: '<angle>'; inherits: false; initial-value: 0deg;}现代选择器::is、:where、:has
Section titled “现代选择器::is、:where、:has”/* :is() 合并重复的选择器前缀 */:is(header, footer) a:hover { color: red; }/* 等价于 header a:hover, footer a:hover */
/* :where() 写法相同,但优先级恒为 0,适合写可被轻松覆盖的默认样式 */:where(ul, ol) { padding-left: 1.5em; }
/* :has() 根据后代或后续兄弟选中元素,常被称为"父选择器" */.card:has(img) { padding: 0; }label:has(+ input:invalid) { color: red; }form:has(:invalid) button[type="submit"] { opacity: 0.5; }| 选择器 | 优先级 | 参数中有无效选择器时 |
|---|---|---|
:is() |
取参数中最高者 | 忽略无效项,其余照常匹配 |
:where() |
恒为 0 | 忽略无效项,其余照常匹配 |
:has() |
取参数中最高者 | 整条规则失效 |
:has() 让很多过去必须用 JS 切换类名的场景变成纯 CSS。这几个选择器在主流浏览器的较新版本中都已可用,需要兼容旧浏览器时要先确认目标环境。此外,CSS 现在也原生支持类似 Sass 的嵌套写法(.card { & .title {} })。
- 层叠比较顺序:来源与
!important→ 层叠层 → 优先级 → 出现顺序;优先级按 (ID, 类/属性/伪类, 元素/伪元素) 逐位比较。 - 文字相关属性默认继承,盒子与布局属性不继承;
initial是规范初始值,恢复浏览器默认用revert。 - 全局使用
box-sizing: border-box;垂直外边距会合并,BFC 可以隔断合并、包含浮动,创建 BFC 推荐display: flow-root。 absolute参照最近定位祖先,transform等属性会改变fixed的参照;Flex 子项溢出时检查min-width: 0。z-index只在同一层叠上下文内比较,opacity、transform等都会创建上下文;组件根节点可用isolation: isolate隔离。- CSS 变量在运行时解析、沿 DOM 继承,适合主题切换;
:is/:where/:has分别用于合并选择器、零优先级默认样式和“父选择器”。