继承、层叠和优先级
在软件开发中,CSS 是很特别的存在。严格来讲,它不是编程语言,却要求抽象思维。它不是纯粹的设计工具,却要求创造力。
它提供了看似简单的声明式语法,但是在大型项目中写过 CSS 的人都知道它可能会变得极其复杂。
在学习传统编程中遇到问题时,你通常知道该搜索什么(比如,“如果找到一个数组里类型为 x 的元素”)。
在 CSS 中,却很难将问题提炼成一句话。即使可以,答案一般也是“这得看情况”。最好的解决办法通常取决于具体场景,以及你希望以多大粒度处理各种边缘问题。
继承
有一种给元素添加样式的方式: 继承。
经常有人把层叠跟继承混淆。虽然两者相关,但是应该分别理解它们。
如果一个元素的某个属性没有层叠值,则可能会继承某个祖先元素的值。
比如通常会给 <body>
元素加上 font-family
,里面的所有祖先元素都会继承这个字体,就不必给页面的每个元素明确指定字体了。
下图展示了继承是如何顺着 DOM 树向下传递的。
但不是所有的属性都能被继承。默认情况下,只有特定的一些属性能被爱继承,通常是我们希望被继承的那些。
它们主要是跟文本相关的属性: color、font、font-family、font-size、font-weight、font-variant、font-style、line-height、letter-spacing、text-align、text-indent、text-transform、white-space 以及 word-spacing
还有一些其他的属性也可以被继承,比如列表属性: list-style、list-style-type、list-style-position 以及 list-style-image。
表格的边框属性 border-collapse 和 border-spacing 也能被继承。
注意,这些属性控制的是表格的边框行为,而不是常用于指定非表格元素边框的属性。(恐怕没人希望将一个 <div>
的边框传递到每一个后代元素)。
以上为不完全枚举,但是已经很详尽了。
我们可以在适当的场景使用继承。比如给 body 元素应用字体,让后代元素继承该字体
1 2 3 |
|
将属性加到 body 上会让整个网页上生效。而将属性加到特定元素上,则只会被它的后代元素继承。
继承属性会顺序传递给后代元素,直到它被层叠值覆盖
继承似乎有点难以理解,不过却可以节省很多时间。
假设 CSS 属性不会传给内部嵌套的标签,如果一段文字中包含其他标签,例如用于强调的 <strong>
和 <em>
标签,用于添加链接的<a>
标签。
我们定义了一个样式,让这段文字显示成白色,字高为 32 像素,使用 VarelaRound 字体。如果<em>
、<strong>
和<a>
标签里的文字还使用默认的单调样式,一定很奇怪。
为了统一,我们还得再定义一个样式,让<em>
标签的外观与<p>
标签匹配。这样太麻烦了!
继承不仅适用于标签样式,任何类型的样式都能继承。所以,如果在某个标签上应用类样式,标签里的标签会从类样式中继承属性。
继承如何简化样式表
使用继承可以简化样式表。假如想让网页中的所有文字都使用相同的字体,不用分别为每个标签定义样式,只需为<body>
标签定义一个标签样式即可(也可以定义一个类样式,然后将其应用到<body>
标签上)。
在这个标签样式中指定想用的字体,网页中所有其他的标签都会继承这个字体属性:
1 2 3 |
|
使用继承还能把样式属性应用到网页中的某个区域上。例如,很多 Web 设计师会使用<div>
标签标识网页中的不同区域,例如横幅、侧边栏和页脚;
如果使用 HTML5 的话,可能会使用某个分区标签,例如<header>
、<aside>
、<footer>
或<article>
。
在为外层标签定义的样式中,某些 CSS 属性会应用到区域内的所有标签上。如果想让侧边栏中的文字使用相同的颜色,可以在样式中设置 color 属性,然后把样式应用到<div>
、<header>
或<article>
等分区标签上。
此时,内部嵌套的<p>
和<h1>
等标签都会继承相同的文字颜色。
继承的局限性
美中不足,继承也有缺点: 很多 CSS 属性根本不会传给后代标签。例如,border 属性(在元素四周画边框)就不会继承。当然,这是有道理的。
如果继承,设置了 border 属性的元素里每个标签都会有边框。比如说为 <body>
标签设置了边框,那么每个无序列表和列表里的每个项目都会有边框。
下面举些例子,说明什么时候并不一定会继承:
- 一般来说,影响元素在页面中所在位置的属性,以及设置元素外边距、背景色和边框的属性不会被继承
- Web 浏览器渲染标签时会继承各自的内部样式: 标题使用大号粗体字,链接是蓝色,等等。即便为网页中的文字定义了字号,并将其应用到
<body>
标签上,标题仍然比段落里的文字大,<h1>
标签也仍然比<h2>
标签的字号大。同理,即便为<body>
标签定义了文字颜色,网页中的链接仍然使用 Web 浏览器默认定义的蓝色。
注意: 通常最好不用浏览器内置的样式,这样网站的外观在不同的浏览器中才能尽量保持一致
- 样式冲突时,更具体的样式胜出。也就是说,如果专门为某个元素设置了 CSS 属性,例如为无序列表指定了字号,而专门设置的属性与继承的属性(例如为
<body>
标签设置的font-size
属性)冲突了,那么浏览器会使用为<ul>
标签设置的字号
注意: 样式经常出现这种冲突,浏览器处理这种冲突的规则称为“层叠”
使用开发者工具
当属性值被继承和覆盖时,这个路径会很难追踪。如果你还不熟悉浏览器的开发者工具,请开始养成使用它们的习惯。
使用开发者工具能够看到哪些元素应用了哪些样式规则,以及为什么应用这些规则。
层叠和继承都是抽象的概念,使用开发者工具是最好的追踪方式。
在一个页面元素上点击鼠标右键,选择弹出菜单上的检查元素,就能打开开发者工具,示例如下所示。
样式检查器显示了所检查元素的每个选择器,它们根据优先级排列。
在选择器下方是继承属性。元素所有的层叠和继承一目了然。
有很多细节可以帮助开发人员弄清楚一个元素的样式是怎么产生的。
靠近顶部的样式会覆盖下面的样式。被覆盖的样式上划了删除线。
右侧显示了每个规则集的样式表和行号,你可以在源代码中找到它们。
这样就能准确判断哪个元素继承了哪些样式以及这些样式的来源。还可以在顶部的筛选框中选择特定的声明,同时隐藏其他声明。
继承示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
这个网页的外观整体看起来很统一,不过仔细观察,你会发现类样式中设置的颜色只影响了标题和无序列表;而且,即便设置了字号,标题文字的大小还是与段落不同。
我们在每个段落的左侧都添加了边框,而且把文字向右移了一点,防止文字与边框连在一起。
注意,所有<p>
标签的左侧都有一个淡色粗边框。可是,<p>
标签里的标签(例如<em>
标签)没有缩进,也没有边框。
这种行为是合理的,如果段落里每个<em>
和<strong>
标签都有粗边框,而且边框之间有 20 像素间隔,那就太奇怪了。
层叠
CSS 本质上就是声明规则,即在各种条件下,我们希望产生特定的效果。如果某个元素有这个类,则应用这些样式。
如果 X 元素是 Y 元素的子节点,则应用那些样式。浏览器会根据这些规则,判断每个规则应该用在哪里,并使用它们去渲染页面。
如果只看几个小例子,CSS 的规则很容易理解。
但是当样式表变大,或者将同一份样式表应用到更多的网页时,CSS 代码很快就会变得复杂。在 CSS 里实现一个效果通常有好几种方式。
当 HTML 结构变化,或者将同一份样式表应用到不同的网页时,不同的实现方式会产生不同的结果。
样式冲突时会出现一些特别怪异的事情,比如说,明明在类样式中把文字设为红色了,可以最终显示的却是天蓝色。
幸好,CSS 会使用一种机制处理冲突。这种机制叫层叠(cascade),用于管控样式之间相互作用的方式,出现冲突时判定哪个样式的优先级高。
CSS 开发很重要的一点就是以可预测的方式书写规则。
首先我们需要理解浏览器如何解析样式规则。每条规则单独来看很简单,但是当两条规则提供了冲突的样式时会发生什么呢?
如果你发现有一条规则没有按照预期生效,可能是因为另一条规则跟它冲突了。
要想预测规则最终的效果,就需要理解 CSS 里的层叠。
为了演示,你需要构建一个简单的网页头部,如下图所示。上面是网站标题,下面是一排蓝绿色的导航链接。
最后一个链接是橘黄色的,用来表示其特殊性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
对同一个元素应用多个规则时,规则中可能会包含冲突的声明。上面的代码就展示了这一点。
它包含三个规则集,每一个给网页标题指定了不同的字体样式。标题不可能同时显示三种样式。哪一个会生效呢?
浏览器为了解决这个问题会遵循一系列规则,因此最终的效果可以预测。在上面的例子里,规则决定了第二个声明(即 ID 选择器)生效,因此标题采用 sans-serif 字体
层叠指的就是这一系列规则。它决定了如何解决冲突,是 CSS 语言的基础。虽然有经验的开发人员对层叠有大体的了解,但是层叠里有些规则还是容易让人误解。
下面来分析层叠的规则。当声明冲突时,层叠会依据三种条件解决冲突。
(1) 样式表的来源: 样式是从哪里来的,包括你的样式和浏览器默认样式等。
(2) 选择器优先级: 哪些选择器比另一些选择器更重要。
(3) 源码顺序: 样式在样式表里的声明顺序
层叠的规则是按照这种顺序来考虑的。下图概括展示了规则的用法
这些规则让浏览器以可预测的方式解决 CSS 样式规则的冲突。
术语解释:
以下是 CSS 中的一行。他被称作一个声明。该声明由一个属性(color)和一个值(black)组成:
1 |
|
不要将 CSS 属性(property) 跟 HTML 属性(attribute)混淆。比如在 <a href="/">
元素里,href 就是 a 标签的一个 HTML 属性。
包含在大括号内的一组声明被称作一个声明块。声明块前面有一个选择器(如下面的 body)。
1 2 3 4 |
|
选择器和声明块一起组成了规则集(ruleset)。
一个规则集也简称一个规则,不过我发现很少有人说单数形式的规则(rule),通常会用复数形式(rules),用来指一系列样式的几何。
最后,@规则(at-rules)是指用 "@"
符号开头的语法。
比如 @import
规则或者 @media
查询
样式表的来源
你添加到网页里的样式表并不是浏览器唯一使用的样式表,还有其他类型或来源的样式表。
你的样式表属于作者样式表,除此之外还有用户代理样式表,即浏览器默认样式。
用户代理样式表优先级低,你的样式会覆盖它们。
用户代理样式在不同浏览器上稍有差异,但是大体上是在做相同的事情: 为标题 <h1>
和 <h6>
和段落 <p>
添加上下外边距,为列表 <ol>
和 <ul>
添加左侧内边距,为链接添加颜色,为元素设置各种默认字号。
用户代理样式
再看一下示例的网页。标题字体是 sans-serif,由你添加的样式决定。
其他元素的样式则是由用户代理样式决定: 列表有左侧内边距,list-style-type
为 disc
,因此有项目符号(小黑点)。
链接为蓝色且有下划线。标题和列表有上下外边距。
浏览器应用了用户代理样式后才会应用你的样式表,即作者样式表。
你指定的声明会覆盖用户代理样式表里的样式。如果你在 HTML 里面链接了多个样式表,那么它们的来源都相同,即作者。
用户代理样式表因为设置了用户普遍需要的样式,所以不会做一些完全超出预期的事情。
当你不喜欢默认样式时,可以在自己的样式表里设置别的值。
现在就试试覆盖一些你不想要的用户代理的样式,让网页看起来如下图所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
|
如果长期使用 CSS,你大概习惯了覆盖用户代理的样式。
这种做法实际上就是利用了层叠的样式来源不同,你写的样式会覆盖用户代理样式,因为来源不同。
说明: 上面用了 ID 选择器,但应该避免使用这种选择器
!important 声明
样式来源规则有一个例外: 标记为重要(important) 的声明。
如下所示,在声明的后面、分号的前面加上 !important
,该声明就会被标记为重要的声明。
1 |
|
标记了 !important
的声明会被当作更高优先级的来源,因此总体的优先级按照由高到低排列如下所示:
(1) 作者的 !important
(2) 作者
(3) 用户代理
层叠独立地解决了网页中每个元素的样式属性的冲突。
例如,如果给段落设置加粗的字体,用户代理的上下外边距样式仍然会生效(除非被明确覆盖)。
处理过渡和动画时,还会再提到样式来源的概念,因为它们会引入更多的来源。
!important
注释是 CSS 的一个有趣而怪异的特性。
理解优先级
如果无法用来源解决冲突声明,浏览器会尝试检查它们的优先级。
理解优先级很重要。不理解样式的来源照样可以写 CSS,因为 99% 的网站样式是来自同样的源。
但是如果不理解优先级,就会被坑得很惨。不幸的是,很少有人提及这个概念。
浏览器将优先级分为两部分: HTML 的行内样式和选择器的样式。
行内样式:
如果用 HTML 的 style 属性写样式,这个声明只会作用于当前元素。
实际上行内元素属于“带作用域的”声明,它会覆盖任何来自样式表或者 <style>
标签的样式。
行内样式没有选择器,因为它们直接作用于所在的元素。
在示例中,需要让导航菜单里的特殊链接变成橘黄色,有好几种方式能够实现这种效果。
首先使用行内样式
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
如果要在样式表里覆盖行内声明,需要为声明添加 !important
,这样能将它提升到一个更高优先级的来源。
但如果行内样式也被标记为 !important
,就无法覆盖它了。最好是只在样式表内用 !important
。
选择器优先级
优先级的第二部分由选择器决定。比如,有两个类名的选择器比只有一个类名的选择器优先级更高。
如果一个声明将背景色设置为橘黄色,但另一个更高优先级的声明将其设置为蓝绿色,浏览器就会将蓝绿色应用到元素上。
为了演示,我们尝试用一个简单的类选择器将特殊链接设置为橘黄色。
1 2 3 4 5 6 7 8 9 10 11 |
|
没有生效!所有的链接仍然是蓝绿色。为什么呢?第一个选择器的优先级高于第二个选择器。
第一个由一个 ID 和一个标签名组成,而第二个由一个类名组成。但选择器的长度并不是决定优先级的唯一因素。
不同类型的选择器有不同的优先级。比如,ID 选择器比类选择器优先级更高。
实际上,ID 选择器的优先级比拥有任意多个类的选择器都高。
同理,类选择器的优先级比标签选择器(也称类型选择器)更高。
优先级的准确规则如下:
- 如果选择器的 ID 数量更多,则它会胜出(即它更明确)
- 如果 ID 数量一致,那么拥有最多类的选择器胜出
- 如果以上两次比较都一致,那么拥有最多标签名的选择器胜出
按照优先级由低到高排列的选择器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
最明确的选择器是有 1 个 ID 的(4),因此标题的颜色最终为红色。第二明确的是有 2 个类的(3)。如果没有出现带 ID 选择器的 (4),则 (3) 的声明会生效。选择器(3)比选择器 (2) 的优先级更高,尽管选择器 (2) 更长: 2 个类比 1 个类更明确。
最后,选择器 (1) 最不明确,它有 4 个元素类型(即标签名),但是没有 ID 或者类
说明:
伪类选择器(如: hover)和属性选择器(如[type="input"]
) 于一个类选择器的优先级相同。
通用选择器(*
)和组合器(>、+、~
)对优先级没有影响。
如果你在 CSS 里写了一个声明,但是没有生效,一般是因为被更高优先级的规则覆盖了。
很多时候开发人员使用 ID 选择器,却不知道它会创建更高的优先级,之后就很难覆盖它。如果要覆盖一个 ID 选择器的样式,就必须要用另一个 ID 选择器。
这个概念很简单,但是如果你不理解优先级,就无法弄清楚为什么一饿规则能生效,另一个却不能。
优先级标记
一个常用的表示优先级的方式是用数值形式来标记,通常用逗号隔开每个数。比如,"1,2,2" 表示选择器由 1 个 ID、2 个类、2 个标签组合。
优先级最高的 ID 列为第一位,紧接着是类,最后是标签。
选择器 #page-header #page-title
有 2 个 ID,没有类,也没有标签,它的优先级可以用 "2,0,0" 表示。
选择器 ui li
有 2 个标签,没有 ID,也没有类名,它的优先级可以用 "0,0,2" 表示。
有时,人们还会用 4 个数的标记,其中将最重要的位置用 0 或 1 来表示,代表一个声明是否是用行内样式添加的。
此时,行内样式的优先级为 "1,0,0,0"。
它会覆盖通过选择器添加的样式,比如优先级为 "0,1,2,0"(1 个 ID 和 2 个类)的选择器
关于优先级的思考
之前常使用 .feature
选择器添加橘黄色背景,但是没有成功。
#main-nav a
选择器包含了一个 ID,覆盖了类选择器(优先级分别为 "1,0,1" 和 "0,1,0")。
有好几种方法可以解决这个问题。下面介绍几种可行的方法。
最快的方法是将 !important
添加到想要设置的元素的声明上。
1 2 3 |
|
这个方法之所以生效,是因为 !important
注释将声明提升到了更高优先级的来源。这个方法的确简单,但也很低级。
它可能解决了眼前的问题,但是会在以后带来更多问题。
一旦给很多声明加上 !important
,要覆盖已设置为 important 的声明时,该怎么做呢?
当给一些声明加上 !important
时,就会先比较来源,再使用常规的优先级规则。
最终会让一切回到起点: 一旦引入一个 !important
,就会带来更多的 !important
那么更好的方法是什么?请不要试图绕开选择器优先级,而是利用它来解决问题。
何不提升选择器的优先级呢?将你的 CSS 修改为下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
这个方法也奏效了。现在你的选择器有一个 ID 和 1 个类,优先级为 “1,1,0”,比 #main-nav a
(优先级为 "1,0,1")高。
因此,橘黄色背景是能够应用到元素上的。
但是这个方法还能改进。不提升第二个选择器的优先级,而是降低第一个选择器的优先级。
导航链接元素同时还有一个类: <ul id="main-nav" class="nav">
。
所以可以修改 CSS,通过类名而不是 ID 来选中元素。将选择器里的 #main-nav
改为 .nav
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
现在你已经降低了这些选择器的优先级。橘黄色背景的优先级足够高,能够覆盖蓝绿色
通过这些例子可以发现,优先级容易发展为一种“军备竞赛”。在大型项目中这一点尤为突出。
通常最好让优先级尽可能低,这样当需要覆盖一些样式时,才能有选择空间。
源码顺序
层叠的第三步,也是最后一步,是源码顺序。
如果两个声明的来源和优先级相同,其中一个声明在样式表中出现较晚,或者位于页面较晚引入的样式表中,则该声明胜出。
也就是说,可以通过控制源码顺序,来给特殊链接添加样式。
如果两个冲突选择器的优先级相同,则出现较晚的那个胜出。
1 2 3 4 5 6 7 8 9 10 11 12 |
|
在这个方法里,选择器优先级相同。
源码顺序决定了哪个声明作用域特殊链接,最终产生了橘黄色的特殊按钮
这个方法解决了问题,但也引入了一个潜在的新问题: 虽然在 nav 元素里的特殊按钮看起来正常了,但是如果你想要在页面其他地方,在 nav 之外的链接上使用 featured 类呢?
最后就会有奇怪的混合样式: 橘黄色的背景,但是导航链接没有文本颜色、内边距或者圆角效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
有一个元素只被第二个选择器选中,没有被第一个选中,因而没有产生期望的结果。
你得决定是否让 nav 以外的元素拥有橘黄色的按钮样式,如果是,需要确保将所有想要的样式都应用到元素上。
除非网站有其他需求,否则我倾向于方法三。理想状态下,你可以凭经验判断在页面其他地方会出现什么样式需求。
也许你知道别处也可能想要一个特殊链接,这种情况下,也许方法四更合适,当然在别处还需要添加一些样式来补充 feature 类。
正如之前所说,在 CSS 中最好的答案通常是“这得看情况”。实现相同的效果有很多途径。
多想些实现方法,并思考每一种方法的利弊,这是很有价值的。面对一个样式问题时,我经常分两个步骤来解决它。
首先确定哪些声明可以实现效果。其次,思考可以用哪些选择器结构,然后选择最符合需求的那个。
链接样式和源码顺序
你刚开始学习 CSS 时,或许就知道给链接加样式要按照一定的顺序书写选择器。这是因为源码顺序影响了层叠。
下面代码展示了如何以 "正确" 的顺序书写链接样式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
|
书写顺序之所以很重要,是因为层叠。优先级相同时,后出现的样式会覆盖先出现的样式。
如果一个元素同时处于两个或者更多状态,最后一个状态就能覆盖其他状态。
如果用户将鼠标悬停在一个访问过的链接上,悬停效果会生效。如果用户在鼠标悬停时激活了链接(即点击了它),激活的样式会生效。
这个顺序的记忆口诀是 "LoVe/HAte"("爱/恨",即 link(链接)、visited(访问)、hover(悬停)、active(激活)。
注意,如果将一个选择器的优先级改得跟其他的选择器不一样,这个规则就会遭到破坏,可能会带来意想不到的结果。
层叠值
浏览器遵循三个步骤,即来源、优先级、源码顺序,来解析网页上每个元素的每个属性。
如果一个声明在层叠中“胜出”,它就被称作一个层叠值。元素的每个属性最多只有一个层叠值。
网页上一个特定的段落(<p>
)可以有一个上外边距和一个下外边距,但是不能有两个不同的上外边距或两个不同的下外边距。
如果 CSS 为同一个属性指定了不同的值,层叠最终会选择一个值来渲染元素,这就是层叠值。
层叠值: 作为层叠结果,应用到一个元素上的特定属性的值
如果一个元素上始终没有指定一个属性,这个属性就没有层叠值。
还是拿段落举例,可能就没有指定的边框或者内边距。
两条经验法则
你可能知道,处理层叠时有两条通用的经验法则。因为它们很有用,所以提一下。
(1) 在选择器中不要使用ID。就算只用一个 ID,也会大幅提升优先级。当需要覆盖这个选择器时,通常找不到另一个有意义的 ID,于是就会复制原来的选择器,然后加上另一个类,让它区别于想要覆盖的选择器。
(2) 不要使用 !important
。它比 ID 更难覆盖,一旦用了它,想要覆盖原先的声明,就需要再加上一个 !important
,而且依然要处理优先级的问题。
这两条规则是很好的建议,但不必固守它们,因为也有例外。不要为了赢得优先级竞赛而习惯性地使用这两个方法
关于重要性的一个重要提醒:
当创建一个用于分发的 JavaScript 模块(比如 NPM包) 时,强烈建议尽量不要在 JavaScript 里使用行内样式。
如果这样做了,就是在强迫使用该包的开发人员要么全盘接受包里的样式,要么给每个想修改的属性加上 !important
正确的做法是在包里包含一个样式表。
如果组件需要频繁修改样式,通常最好用 JavaScript 给元素添加或者移除类。
这样用户就可以在使用这份样式表的同时,在不引入优先级竞赛的前提下,按照自己的喜好选择编辑其中的样式。
过去几年涌现了一些实践方法,能够帮助我们管理选择器选择器。
包括如何处理优先级,以及在哪里可以放心使用!important
。
样式层叠的方式
层叠是指一系列规则,用于确定把哪些样式属性应用到元素上。
Web 浏览器通过层叠规则判断如何处理应用于同一个标签上的多个样式,确定如何处理有冲突的 CSS 属性。
以下两种情况会导致样式冲突: 继承,从多个祖辈那里继承相同的属性;
同一个元素有多个样式(例如为某个段落定义了类样式,又定义了<p>
标签样式,此时这两个样式都会应用到那个段落上)
多个继承的样式和专为标签定义的样式顺利地组合在一起,形成了混合样式。可以,如果继承的 CSS 属性有冲突怎么办?
假如我们把<body>
标签的文字样色设置为红色,把<p>
标签的文字颜色设为绿色。
如果段落里有个<strong>
标签,这个标签会从<body>
和<p>
标签那里继承样式,那么<strong>
标签里的文字会显示成红色还是绿色呢?
女士们先生们,胜出的是 -- 段落样式里设定的绿色。这是因为 Web 浏览器会采用离标签最近的祖辈的样式
在这个例子中,从<body>
标签继承的属性更具一般性,对所有的标签都有效。而应用在你<p>
标签上的样式更具针对性,只对<p>
标签和内部的标签有效
简单来说,如果没有专门为标签定义样式,继承的属性出现冲突时,最近的祖辈胜出
再举一个例子。如果某个 CSS 样式把<table>
标签里的文字定义为一个颜色,另一个样式把<td>
标签里的文字定义为其他颜色,
那么单元格(<td>
标签)里的标签,例如段落、标题和无序列表,都会使用为<td>
标签定义的文字颜色,因为<td>
标签是最近的祖辈
由“最近的祖辈胜出”规则可以推理出,在 CSS 继承体系中有一种样式始终会胜出--直接应用在标签上的样式。
假如<body>
、<p>
和<strong>
标签都设置了颜色,<p>
标签样式比<body>
标签样式具体,<strong>
标签样式则比<body>
和<p>
标签样式都具体。
<strong>
标签样式只应用在<strong>
标签上,而且会覆盖从其他标签继承的有冲突的属性。也就是说,标签专用样式里的属性会击败所有继承的属性。
这个规则解释了为什么有些继承的属性看起来像没继承一样,例如,段落的文字是红色,而链接的颜色仍然是浏览器默认的蓝色。
这是因为浏览器为<a>
标签预设了样式,所以链接不会使用继承的文本颜色。
一个标签,多个样式
继承是多个样式影响一个标签的方式之一。除此之外,还可以把多个样式直接应用到一个标签上。
例如,一个外部样式表为<p>
标签定义了样式,链接这个外部样式表的网页中还有内部样式表,而且内部样式表也为<p>
标签定义了样式。
更有趣的是,这个页面中有个<p>
标签应用了类样式。那么,现在有三个样式直接应用在这个<p>
标签上。遇到这种情况时,浏览器会采用哪个(或哪些)样式呢?
答案是,视情况而定。根据样式的类型,以及定义各个样式的顺序,浏览器一次可能会使用一个或多个样式。下面列出一次使用多个样式的几种情况:
- 使用标签选择符定义了样式,又把类样式应用到标签上。例如,为
<h2>
标签定义了标签样式,又使用下述 HTML 代码把.leadHeadline
类样式应用到这个标签上:<h2 class="leadHeadline">Your Future Revealed!</h2>
,此时,两个样式都会应用到这个<h2>
标签上 - 相同的样式名在样式表中出现多次。例如,样式表中有一个群组选择符
.leadHeadline, .secondaryHeadline, .newsHeadline
,还有一个类样式.leadHeadline
。 这两个规则定义的样式都会应用到 class 属性的值中包括leadHeadline
的元素上 - 既有类样式,又有 ID 样式的标签。例如,ID 为
#banner
,类为.news
,HTML 代码为<div id="banner" class="news">
。此时,#banner
样式和.news
样式都会应用到<div>
标签上 - 一个网页用到了多个样式表,而且各个样式表中都有名称相同的样式。这些名称相同的样式可以在外部样式表中,也可以在内部样式表中。或者在网页链接的多个外部样式表中
- 多个复杂的选择符选取相同的标签。使用后代选择符时时经常会出现这种情况。比如说网页中有个
<div>
标签(例如<div id="mainContent">
),这个<div>
标签里有个段落,而且这个段落设置了类:<p class="byline">
。
那么,下述选择符都能选取这个段落:css #mainContent p #mainContent .byline p.byline .byline
如果一个元素有多个样式,Web 浏览器会合并这些样式的属性,不过前提是,样式之间没有冲突。下面举个例子,说明这个规则。
假设网页中有一段文字的内容是作者名,而且链接到作者的电子邮件地址。这个段落的 HTML 代码可能是下面这样:
1 |
|
而这个网页的样式表中有三个用于装饰链接外观的样式:
1 2 3 4 5 6 7 8 9 |
|
第一个样式把所有<a>
标签的颜色都设为深蓝色,第二个样式让<p>
标签里的<a>
标签都以粗体显示,第三个样式把类为byline
的元素中链接的下划线去掉。
这三个样式都会应用到那个电子邮件地址链接上。因为这些样式中没有相同的属性,所以不会冲突。
特指度:确定哪个样式胜出
前面那个示例特别易于理解。但是,如果那三个链接样式设置了不同的 font-family
属性呢?Web 浏览器会使用哪个样式里设定的字体呢?
如果你仔细阅读了前文,会发现,层叠提供了一套帮助浏览器解决属性冲突的规则,即最具体的样式里的属性胜出。可是,就像前面那几个样式一样,有时无法确定哪个样式更具体。
幸好,CSS 提供了一个公式,根据赋给各种选择符(标签选择符,类选择符,ID 选择符)的值计算样式的特指度(specificity)。赋给各种选择符的值如下:
- 一个标签选择符记 1 分
- 一个类选择符记 10 分
- 一个 ID 选择符记 100 分
- 一个行内样式记 1000 分
注意: 计算特指度的方式比这里所述的要复杂一点,不过除了最诡异的情况之外,都适用这个公式。
计算得到的值越大,特指度越高。假如我们编写了下述三个样式:
- 为
<img>
标签定义一个标签样式(特指度: 1) - 一个名为
.highlight
的类样式(特指度: 10) - 一个名为
#logo
的 ID 样式(特指度: 100)
假如网页中有这行 HTML 代码: <img id="logo" class="highlight" src="logo.gif" />
。
如果在三个样式中都定义了相同的属性,例如 border 属性,那么,ID 样式(#logo
)里的值胜出
注意: 计算特指度时,伪元素(如::first-line
)与标签选择符一样,记 1 分;伪类(如:link
)与类选择符一样,记 10 分
后代选择符由多个选择符组成(如#content p
或h2 strong
),计算方式要复杂些。后代选择符的特指度是其中各个选择符的得分总和。
注意: 继承的属性没有特指度。所以,即使一个标签从特指度高的样式(如#banner
)中继承了属性,继承的属性也会被直接应用在标签上的样式覆盖。
特指度相同时后一个样式胜出
属性有冲突的两个样式可能有相同的特指度。
如果两个地方使用相同的选择符,特指度会打成平局,例如,内部样式表和外部样式表中都有 p 标签选择器,此时特指度就一样。
当然,两个不同的样式也可能有相同的特指度。特指度一样时,样式表中后面出现的样式胜出。
我们以下述 HTML 代码举个棘手的例子:
1 |
|
包含上述段落和链接的网页,其样式表中有下面两个样式:
1 2 3 4 5 6 |
|
这两个样式的特指度都是 11(类选择符 10 分,标签选择符 1 分),而且都应用在那个<a>
标签上。因此,这两个样式的特指度打成了平局。
那么,浏览器使用哪个颜色装饰上述段落里的链接呢?答案是,红色,因为设定红色的那个属性在第二个样式中(即在后面的样式中)
现在,调换两个样式的位置,把样式包改成下面这样:
1 2 3 4 5 6 |
|
此时,链接的颜色会变成蓝色。因为p .email
样式在.byline a
样式之后,所以p .email
样式里的属性胜出
如果外部样式表里的规则与内部样式表里的规则冲突了呢?遇到这种情况时,样式表(在HTML文件里)的位置特别重要。
如果先使用<style>
标签添加内部样式表,而后再使用<link>
标签链接外部样式表,那么,外部样式表里的样式胜出(即后面的样式胜出)。
我们要尽量把外部样式放在固定的位置,而且最好先链接外部样式表。仅当需要在单个页面中添加样式时才使用内部样式表。
控制层叠
可以看出,CSS 样式变多之后,格式变乱的可能性会随之增大。比如说我们编写了一个类样式,指定字体和字号,可是应用到段落上之后,没有任何效果。
这种问题通常与层叠有关。你可能以为在标签上设置了类就会应用类样式里的格式属性,然而,如果有特指度更高的样式,情况可能就不一样了。
这种问题的处理方式有好几种。其一,可以使用!important
,确保属性始终有效。可以这种方法有点笨拙,因为无法预知什么时候要压制这种效果。除此之外还有两种方法
- 改变特指度
- 有选择地覆盖:
我们还可以有选择地覆盖样式,精确调整特定页面的外观设计。比如说,我们编写了一个名为styles.css
的外部样式表,应用于网站里的每个页面。
这个样式表中定义的是网站的整体外观,包括<h1>
标签的字体和颜色,表单元素的外观等。
可是,我们或许想让首页的<h1>
标签与众不同,例如更粗更大,可能还想把段落里的文字变得更小,以便显示更多的信息。
也就是说,我们仍然想使用styles.css
文件里的大多数样式,只不过想覆盖几个标签(如<h1>
,<p>
等)的部分属性。
这种需求的一种处理方式是使用内部样式表,列出想覆盖的样式。假如styles.css
文件里有如下规则:
css h1 { font-family: Arial, Helvetica, sans-serif; font-size: 24px; color: #000; }
如果想让首页里的<h1>
标签字号变大,显示成红色,在首页的内部样式表中添加如下样式即可:
css h1 { font-size: 36px; color: red; }
这样,首页里的<h1>
标签会使用Arial字体(根据外部样式表里的样式),不过颜色变成了红色,字号变成了 36 像素(根据内部样式表里的样式)。
提示: 在 HTML 代码的<head>
部分里要先链接外部样式表,再添加内部样式表。这样,样式的特指度相同,内部样式表里的样式才会胜出
另一种处理方式是,再编写一个外部样式表,例如命名为home.css
。首先除了链接styles.css
样式表之外,再链接这个样式表。
home.css
文件中保存的样式名和属性用于覆盖styles.css
文件里相应的样式和属性。为了让这种方式生效,要在styles.css
文件之后链接home.css
文件,如下所示:
html <link rel="stylesheet" href="css/styles.css" /> <link rel="stylesheet" href="css/home.css" />
提示: 还有一种方式能精确调整各个页面的外观: 在不同的页面中为<body>
标签设定不同的类,例如.review
、.story
和.home
,然后使用后代选择符修改各个页面中标签的外观。
避免特指度战争:
如今,很多 Web 设计师都避免使用 ID,而偏爱使用类。其中一个原因是,ID 选择符的特指度太高,不易覆盖。
修改特指度往往会导致特指度战争,致使样式表中充斥着特别长特别复杂的选择符。这种问题最好通过实例说明。假如网页中有如下 HTML 代码片段:
1 2 3 4 5 |
|
我们想让这个<div>
标签里的文字使用红色显示,所以使用后代选择符定义了一个样式,如下所示:
1 2 3 |
|
可是,我们想让类为 special 的那个段落使用蓝色显示。如果仅仅使用类选择符定义样式,无法实现所需的效果:
1 2 3 |
|
我们必须把前面那个类选择符改成下面这样才行:
1 2 3 |
|
可是,这样修改又带来了两个问题: 其一,选择符变长了;其二,仅当 special 类在 ID 为 article 的元素中才会使用蓝色显示。
例如,把<p class="special">A special paragraph</p>
复制粘贴到页面的其他部分,就不会使用蓝色显示。
也就是说,使用 ID 会让选择符变得更长,而作用却降低了。
现在我们来看一下把 ID 改成类之后情况如何。修改之后,前面那段 HTML 代码变成了:
1 2 3 4 5 |
|
此时,可以把 CSS 改成:
1 2 3 4 5 6 |
|
第一个样式使用的是后代选择符.article p
,其特指度为 11.第二个样式使用的选择符是 p.special
,特指度也是 11(一个标签,一个类)。
不过第二个选择符的意思是,"把样式里的属性应用到类为 special 的任何段落上。"现在,把那个段落复制粘贴到网页的任何地方,都会使用蓝色显示。
这只是一个示例,在样式表中不难发现有些选择符长到难以置信,例如#home #article #sidebar #legal p
和#home #article #sidebar #legal p.special
当然,ID 也有用处。例如,很多 CMS(Content Management System,内容管理系统)会使用 ID 标识页面中独一无二的元素,此时就适合使用 ID 选择符。
ID 选择符的特指度高,所以便于覆盖其他样式。不过,ID 选择符不能滥用。大多数情况下,ID 选择符都能替换为简单的类选择符或标签选择符。
ID 选择符的特指度太高,会把样式变得异常复杂。
重设默认样式:
浏览器为各种标签提供了默认的样式,例如: <h1>
标签的字号比<h2>
标签大,而且都是粗体;段落里的文字要小一些,而且不是粗体;
链接是蓝色的,有下划线;无序列表会向右缩进。HTML 标准没有定义任何格式,Web 浏览器提供这些格式是为了让基本的 HTML 更易于阅读。
虽然不同的浏览器为各种标签提供的样式大体相似,但是具体实现各不相同。
例如,Chrome 和 Firefox 使用 padding 属性缩进无序列表,而 Internet Explorer 则使用 margin 属性。除此之外,不同的浏览器为各个标签指定的字号会有细微的差别,而且大多数 Web 浏览器使用的外边距值也不相同。
浏览器之间的这种差异会导致一些问题,例如 Firefox 为某个元素添加了上外边距,而 Internet Explorer 则没有。出现这种问题,责任不在你,而是由浏览器内置样式之间的差异导致的。
为了避免浏览器之间的不一致性,编写样式表时最好统一标准。也就是说,我们要清除浏览器内置的格式,自行指定相应的格式。
清除浏览器内置样式这种行为称为 CSS 重置。接下来简述一种重置方式。
CSS 重置其实就是在样式表开头放一些核心标签的样式,在这些样式中设置在各种浏览器中表现不一致的属性,统一基准。
下面是一段简单的 CSS 重置代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
注意: 上述代码源自 Eric Meyer 制定的 CSS 重置代码。Eric 的 CSS 重置代码广为人知,影响深远,详情请访问: https://meyerweb.com/eric/tools/css/reset/
第一个样式使用一个特别长的群组选择符,把一些最常用的标签还原为本来的面貌--移除所有内外边距,把字号设为 100%,不使用粗体显示文本。
这么做是为了让常用的标签在各个浏览器中具有基本一致的外观,这正是 CSS 重置的意义所在: 统一基准,然后自己编写样式,让 HTML 代码在所有浏览器中都有一致的外观。
第二个样式使用的也是群组选择符article, aside, details,...
,作用是让旧版浏览器正确地显示 HTML5 新引入的标签。
第三个样式(body)设置一致的 line-height 属性(即段落中行与行之间的间距)
除此之外,还可以使用 normalize.css 重置样式。这是一个免费开源的样式表,能让相同的标签在不同的浏览器中具有一致的外观,在 Web 设计圈子里使用广泛。
normalize.css
项目的网站地址是 http://necolas.github.io/normalize.css
第四个和第五个样式(ol 和 ul 标签样式)为列表设置统一的左边距和其他样式。最后一个样式的作用是便于为单元格添加边框
教程:层叠实战
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
|
特殊值
有两个特殊值可以赋给任意属性,用于控制层叠: inherit
和 initial
。
我们来看看这两个特殊值。
使用 inherit 关键字
有时,我们想用继承代替一个层叠值。这时候可以用inherit关键字。
可以用它来覆盖另一个值,这样该元素就会继承其父元素的值。
假设我们要给网页加上一个浅灰色的页脚。
在页脚上有一些链接,但我们不希望这些链接太显眼,因为页脚不是网页的重点。因此要将页脚的链接变成深灰色(如图所示)。
通常我们会给网页的所有链接加上一个字体颜色(如果不加的话,就会以用户代理样式为准)。
这个颜色也会作用于页脚的“Terms of use”链接。为了让页脚的链接变成灰色,需要覆盖颜色值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
|
第三个规则集覆盖了蓝色的链接色,让页脚链接的层叠值为inherit
。因此,它继承了父元素<footer>
的颜色。
这么做的好处是,如果页脚发生任何样式改变的话(比如修改第二个规则集,或者被别的样式覆盖), 页脚链接的颜色就会跟着页脚其他内容一起改变。
比如,当页脚文本变为更深的灰色时,其中的链接也会跟着改变。
还可以使用inherit
关键字强制继承一个通常不会被继承的属性,比如边框和内边距。通常在实践中很少这么做
使用 initial 关键字
有时,你需要撤销作用于某个元素的样式。这可以用initial
关键字来实现。
每一个CSS属性都有初始(默认)值。如果将initial值赋给某个属性,那么就会有效地将其重置为默认值,这种操作相当于硬复位了该值。
下图展示了给页脚链接赋以 initial
而不是 inherit
时的效果。
1 2 3 4 |
|
因为在大多数浏览器中,黑色是color属性的初始值,所以color: initial
等价于color: black
。
这么做的好处是不需要思考太多。如果想删除一个元素的边框,设置 border: initial
即可。
如果想让一个元素恢复到默认宽度,设置 `width: initial 即可。
你可能已经习惯了使用auto
来实现这种重置效果。实际上,用 width: auto
是一样的,因为 width 的默认值就是 auto。
但是要注意,auto不是所有属性的默认值,对很多属性来说甚至不是合法的值。比如 border-width: auto
和 padding: auto
是非法的,因此不会生效。可以花点时间研究一下这些属性的初始值,不过使用initial更简单。
说明:
声明 display: initial
等价于 display: inline
。不管应用于哪种类型的元素,它都不会等于 display: block
。
这是因为 initial
重置为属性的初始值,而不是元素的初始值。inline
才是 display
属性的初始值。
简写属性
简写属性是用于同时给多个属性赋值的属性。比如font是一个简写属性,可以用于设置多种字体属性。
它指定了 font-style、font-weight、font-size、font-height 以及 font-family。
1 |
|
还有如下属性。
- background 是多个背景属性的简写属性:background-color、background-image、background-size、background-repeat、background-position、background-origin、background-chip 以及 background-attachment。
- border 是 border-width、border-style 以及 border-color 的简写属性,而这几个属性也都是简写属性。
- border-width 是上、右、下、左四个边框宽度的简写属性。
简写属性可以让代码简洁明了,但是也隐藏了一些怪异行为。
简写属性会默默覆盖其他样式
大多数简写属性可以省略一些值,只指定我们关注的值。
但是要知道,这样做仍然会设置省略的值,即它们会被隐式地设置为初始值。这会默默覆盖在其他地方定义的样式。
比如,如果给网页标题使用简写属性 font 时,省略 font-weight,那么字体粗细就会被设置为 normal
1 2 3 4 5 6 7 |
|
简写属性等价的展开属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
给 <h1>
添加这些样式会显示成普通的字体,而不是加粗的字体。这些样式也会覆盖从祖先元素继承的字体样式。
在所有的简写属性里,font
的问题最严重,因为它设置的属性值太多了。
因此,要避免在 <body>
元素的通用样式以外使用 font。当然,其他简写属性也可能会遇到一样的问题,因此要当心。
理解简写值的顺序
简写属性会尽量包容指定的属性值的顺序。
可以设置 border: 1px solid black
或者 border: black 1px solid
,两者都会生效。
这是因为浏览器知道宽度、颜色、边框样式分别对应什么类型的值。
但是有很多属性的值很模糊。在这种情况下,值的顺序很关键。理解这些简写属性的顺序很重要。
上、右、下、左
当遇到像 margin
、padding
这样的属性,还有为元素的四条边分别指定值的边框属性时,开发者容易弄错这些简写属性的顺序。
这些属性的值是按顺时针方向,从上边开始的。
记住顺序能少犯错误。它的记忆口诀是 TRouBLe:top(上)、right(右)、bottom(下)、left(左)
。
用这个口诀给元素设置四边的内边距。如图所示的链接,上内边距为 10px,右内边距为 15px,下内边距为 0,左内边距为 5px。
虽然这些内边距看起来不是很均匀,但是可以说明简写属性的顺序。
1 2 3 4 5 6 7 |
|
这种模式下的属性值还可以缩写。如果声明结束时四个属性值还剩一个没指定,没有指定的一边会取其对边的值。
指定三个值时,左边和右边都会使用第二个值。指定两个值时,上边和下边会使用第一个值。
如果只指定一个值,那么四个方向都会使用这个值。因此下面的声明都是等价的。
1 2 3 |
|
下面的声明也是等价的。
1 2 3 4 |
|
对很多开发人员而言,比较难的是指定三个值时。记住,这种情况指定了上、右、下的值。
因为没有指定左边的值,所以它会取与右边相等的值。第二个值就会作用到左边和右边。
因此 padding: 10px 15px 0
是设置左右内边距为 15px,上内边距为 10px,下内边距为 0。
不过,大多数情况只需要指定两个值。尤其对于较小的元素,左右的内边距最好大于上下内边距。这种样式很适合网页的按钮或者导航链接,如图所示:
1 2 3 4 5 6 7 |
|
因为很多属性遵循这个顺序,所以最好记住它
水平、垂直
"TRouBLe" 口诀只适用于分别给盒子设置四个方向的值的属性。
还有一些属性只支持最多指定两个值,这些属性包括 background-position
、box-shadow
、text-shadow
(虽然严格来讲它们并不是简写属性)。
这些属性值的顺序跟 padding 这种四值属性的顺序刚好相反。
比如,padding: 1em 2em
先指定了垂直方向的上/下属性值,然后才是水平方向的右/左属性值,而 background-position: 25% 75%
则先指定水平方向的右/左属性值,然后才是垂直方向的上/下属性值。
虽然看起来顺序相反的定义违背了直觉,原因却很简单:这两个值代表了一个笛卡儿网格。
笛卡儿网格的测量值一般是按照 x, y(水平,垂直)的顺序来的。比如,如图所示,要给元素加上一个阴影,就要先指定x(水平)值。
1 2 3 4 5 |
|
第一个(较大的)值指定了水平方向的偏移量,第二个(较小的)值指定了垂直方向的偏移量。
如果属性需要指定从一个点出发的两个方向的值,就想想“笛卡儿网格”。如果属性需要指定一个元素四个方向的值,就想想“时钟”。
总结
- 控制选择器的优先级。
- 不要混淆层叠和继承。
- 某些属性会被继承,包括文本、列表、表格边框相关的属性。
- 不要混淆
initial
和auto
值。 - 简写属性要注意
TRouBLe
的顺序,避免踩坑。