上一篇你把 HTML 写成了一棵 DOM 树。那棵树只说清了“页面上有什么、谁包含谁”,一个字也没说它们长什么样。CSS 就是给这棵树上每个节点设样式的一套规则。
新手写 CSS 时最常卡住的三类问题,正好对应三件事:
- 样式写在哪里?写在
<link>里、<style>里还是标签的style属性里,有区别吗? - 一条规则会选中哪些元素?
- 两条规则都选中同一个元素、又都想设同一个属性,最后听谁的?
这三件事串起来就是“CSS 怎么落到元素上”。下面按这个顺序讲。
一条 CSS 规则长什么样
1p {
2 color: gray;
3 font-size: 16px;
4}p 是 选择器(selector),负责圈出“这条规则管哪些元素”。花括号里是 声明块,里面每一条 属性: 值; 叫一条 声明(declaration),一条声明只管一个属性。
这里有个容易被忽略但后面很关键的性质:规则不是一个整体。将来发生冲突时,浏览器是逐条声明、按属性分开比的。一条规则里的 color 输了,同一条规则里的 font-size 照样能生效。
样式可以放在三个地方
外部样式表,放在 <head> 里:
1<link rel="stylesheet" href="style.css" />写在一个独立文件里,多个页面共用,改一处全站生效。这是实际项目里的常规做法。
内部样式表,直接把 CSS 写在页面的 <style> 里:
1<style>
2 p { color: gray; }
3</style>只有这个页面能用,适合练手和单页小效果。
行内样式,写在开始标签的 style 属性里:
1<p style="color: gray;">这是一段话。</p>只能作用在这一个元素上,没法复用。
三者的共同点是:最终都会变成“某些元素的某些属性上的一条声明”。真正的差别有两点。
第一,行内样式很特殊。它没有选择器,只有值,浏览器把它当成专属于这一个元素的声明,权重比任何用选择器写出来的正常声明都高。所以它能盖住你的样式表里写的一切——这正是它不该作为常规手段的原因:一旦用了,你只能靠更极端的办法才能改回来。
第二,外部和内部样式表在层叠里地位相同。所以 <head> 里 <link> 和 <style> 谁写在前面、谁写在后面,是会影响结果的,原因在“出现顺序”那一节。
选择器:先圈出一组元素
浏览器对每一条规则做的事是:拿它的选择器去 DOM 树上匹配,匹配上的元素才拿到声明块里的声明 2。看一个例子:
1<ul class="menu">
2 <li><a href="/" class="active">首页</a></li>
3 <li><a href="/about">关于</a></li>
4</ul>1a { color: gray; }
2.menu a { color: navy; }
3.active { font-weight: bold; }a是类型选择器,页面上所有<a>都命中,两个链接都在内。.menu a分两步读:先找class属性里带menu的元素,再在它里面找<a>。只有“首页”那个链接命中。.active找class里带active的元素,也命中“首页”。
这几种写法值得先记住:.x 按 class 选,#x 按 id 选(id 整页唯一),:hover 按状态选(鼠标悬停时才命中)。选择器里的空格表示“在后代里找”。它们全都是筛选条件,本身不设置任何样式——上一篇说 class 和 id 是留给 CSS 的抓手,这里就是落地的样子。
要建立的心智模型是:先匹配,再比权重。一条规则对这个元素要么命中、要么没命中;没命中的规则完全不参与后面的比较。所以你不用去比整个样式表,只比“恰好都命中了这个元素”的那几条。
一个元素的一个属性只有一个值
浏览器渲染时,DOM 树上每个元素的每个属性最终只能取一个值。冲突只发生在“同一个属性的多条声明”之间。
反过来说,不同属性互不干扰。所以“我的样式被覆盖了”这个说法通常不准确:不是整条规则被扔掉了,而是其中某几条声明被压下去了,其余的还在生效。
谁说了算:层叠的三步
CSS 全称 Cascading Style Sheets 里的 Cascading,指的就是这套排序算法。它分步骤进行,前面的步骤分出胜负就不再往下比 2:
- 相关性:选择器命中这个元素了吗?
- 来源与重要性:这条声明来自谁,带没带
!important? - 特异性:同一个来源内部,选择器“有多具体”。
- 出现顺序:前面都没分出高下时,写在后面的赢。
来源。对初学者来说,值得关心的是两类:浏览器自带的默认样式(叫 user-agent 样式表),和你写的作者样式。正常声明里,作者样式优先级更高 2。
这一条直接解释了一个你天天见到、可能没想过为什么的现象:<h1> 默认很大、<p> 默认有上下边距,你只要写一行 p { margin: 0; } 就能改掉。你的规则和浏览器默认规则不在同一个来源上,作者样式直接胜出——不需要 !important,也不需要很“具体”的选择器。这也说明来源这一层排在特异性前面:来源不同时,去比选择器谁更具体是没意义的。
特异性 用来比较同一个来源里的几条规则。它的写法是三列计数:
- ID 列:选择器里用了几个
#id。 - CLASS 列:用了几个
class、属性选择器、伪类(:hover算在这里)。 - TYPE 列:用了几个元素名。
比较时 从左往右:ID 列大的直接赢;ID 列相同才比 CLASS 列;再相同才比 TYPE 列。不是把三列加总分。
回到菜单那个例子。.menu a 是“1 个 class + 1 个元素名”,记作 0-1-1;a 是 0-0-1。链接的 color 两条规则都想设,ID 列都是 0,CLASS 列一个是 1、一个是 0,所以 .menu a 赢——“首页”是深蓝色,与 a 那条规则写在哪一行无关。
再看一个能纠正直觉的例子:body p { color: gray; } 是 0-0-2,.active { color: navy; } 是 0-1-0。类选择器只要出现一个,就赢过任意多个元素名叠加,因为元素名只进第三列,而比较从左往右进行。顺带一个细节:选择器里的空格、> 这类组合符只描述位置关系,不计入任何一列。
出现顺序 负责收尾。特异性完全相同时,后面写的赢:
1.alert { color: orange; }
2.alert { color: green; }绿色生效,因为它在后面。
最后是两个例外。行内样式的属性没有选择器,权重可以看成比任何选择器都高(常写作 1-0-0-0),所以它能盖过上面所有正常声明 1。!important 写在声明的末尾,会在同一个来源内部压过所有不带它的声明,不管特异性多高 3。它危险的地方在于:一旦你用了它,普通声明就再也压不回来,只能再写一个 !important,越写越乱。把它留给“改别人写的、你控制不了的样式”这类少数场合;日常遇到“某个颜色没生效”,先按来源、特异性、顺序这三步查。
没有任何规则命中时
有时候你压根没给某个元素写 color,它的颜色也变了。原因是某些属性会从父元素 继承 下来:你在 body 上写 color: #333,整页文字都跟着变深。
但要区分清楚:继承和层叠是两码事。继承来的值只在 没有任何声明命中这个属性 时才会被用上;只要有一条声明直接命中这个元素,哪怕它只有 0-0-1 这么低的特异性,也是它赢,继承值根本不参与比较 4。
所以“给 body 设了字体,个别元素却不一样”通常不是继承失效,而是那些元素被某条规则直接命中了。
样式不生效时怎么查
上一篇让你用 Elements 面板确认 DOM 树长什么样。这一篇对应的是 Styles 面板:选中一个元素,它会把所有命中这个元素的规则按层叠结果列出来,被压下去的声明会画一道删除线,同时标出它来自哪个文件、哪一行。浏览器默认样式通常列在最上方(标着 user agent stylesheet)。
看到删除线,就说明这条声明输了,接着按上面的顺序问:来源不同吗?特异性谁大?谁写在后面?多数时候三句话之内就能找到原因,不用靠猜或者乱加 !important。
到这里,“CSS 怎么落到元素上”这条链就闭环了:写在哪个位置只是起点,真正决定外观的是“谁命中,以及命中之后层叠怎么排”。