层叠、优先级与继承
- 组成层叠的六个部分
- 层叠与继承的区别
- 样式与元素的对应关系及控制方法
- 简写样式的常见误区
- CSS 新推特性的使用与处理
1.1 层叠
CSS 与软件开发领域的很多东西都不太一样——虽然算不上传统意义上的编程语言,但却要求具备抽象思维;虽然也不是纯粹的设计工具,但却要求具备创造力。CSS 提供了一种看似简单的声明式语法,但是只要写过大型项目 CSS 的人都知道,CSS 可能会变得非常复杂。
在传统编程的学习中遇到问题,通常可以直接搜索出想要的答案(例如:“如何在数组中找出类型为 x 的项?”);而对于 CSS 而言,将遇到的问题提炼为一个可以回答的提问却并非易事。即便可以提炼,最终答案也往往是“看情况”。最好的解决办法通常取决于具体的应用场景,以及该问题的处理需要精确到什么程度、相应要考虑哪些具体的边界条件等。
了解一些“小技巧”或实用秘籍固然有用,但想真正掌握 CSS 这门语言还须理解这些做法背后的原理。本书例证详实,但主要目的还是 CSS 的基本原理。
术语概览
由于 CSS 的学习渠道不同,您或许不熟悉 CSS 语法中各部分的叫法。我不会在此赘述,但由于书中会用到这些称谓,最好还是先弄清它们的含义为上。
以下这一行 CSS 称为一句 声明 。该声明包含一个 属性 ( property
,即 color
)和一个 值 ( value
,即 black
):
color: black;
css1
property
属性不能与 attribute
属性混淆,后者是 HTML 语法的一部分。例如,在元素 <img src="">
中, src
就是 img
标签的 attribute
属性。
包含在大括号内的一组声明被称作一个 声明块 ( declaration block
)。声明块的前面是一个 选择器 ( selector
)。一个选择器指定了页面上的一个或多个元素,它们将被这些声明所修饰。关于选择器类型的全面参考资料,详见 附录 A 。以下是一个 body
选择器的示例,指向 <body>
元素:
body {
color: black;
font-family: Helvetica;
}
css1234
选择器与声明块合在一起,称为一个 规则集 ( ruleset
),也叫一个 规则 ( rule
)。不过据我观察,很少有人像这样精确使用规则的单数形式,通常是用其复数形式来指代一系列样式集合。
最后, @规则 ( at-rules
)是指用一个“@”符号开头的语法结构,比如 @import
导入规则,以及 @media
媒体查询规则。
本书第一部分从 CSS 最基本的原理入手,包括:层叠、盒模型以及各种可用的单位类型。大多数 Web 开发人员都知道层叠和盒模型。他们了解像素单位,可能也听说过“应该换用 em
作单位”。诸如此类的话听得越多,并且一直对这些知识浅尝辄止,终究是走不了多远的。要想彻底拿下 CSS,就必须先理解这些基础知识,而且是深入地理解。
想必您现在一定迫不及待地想学习最新最酷的 CSS 特性了。它们确实令人兴奋,但我们得先回顾一下 CSS 的基础知识。我会快速过一遍,当中可能有您已经熟悉的基础,然后再深入探讨各个话题。本章旨在强基固本,为您后续 CSS 知识体系的构建保驾护航。
本章先从 CSS 中的 C 入手(代表 cascade,层叠)。先阐明其工作原理,再给出实际应用;接着会介绍与层叠相关的话题——继承。然后再谈谈简写属性及其常见的认知误区。
总之,本章话题都是关于如何将特定样式应用到指定元素上的,其中有不少都是开发者填过的坑。全面理解这些话题可以更好地掌控 CSS。幸运的话,您还将更好地认同 CSS 甚至对 CSS 开发乐在其中。
CSS 本质上就是声明规则,并让这些特定的规则在各种情况下生效。一个类添加到某个元素上,则应用这个类包含的这一些样式;元素 X 是元素 Y 的一个子节点,则应用另一些样式。浏览器于是根据这些规则,判定所有样式生效的具体位置,再将它们渲染到页面上。
如果看的都是些简单的示例,这个过程通常都很直观。但随着样式表的不断扩充,或者关联样式的页面一增多,CSS 代码很快就会以惊人的速度变得越来越复杂。在 CSS 里实现一个效果通常有若干种方法。当页面 HTML 结构变更、或者同一份样式被应用到不同的页面时,不同的实现方法会导致截然不同的最终结果。CSS 开发很关键的一点就是确保书写的样式是可预测的。
要做到这一点,首要任务就是理解浏览器究竟是如何解析您书写的样式的。每条规则单拎出来可能简单明了,但要是两条样式有冲突的规则放一起怎么办?某条规则可能因为与另一条规则相冲突而失效。要想预判这些样式规则的最终走向,就必须深入理解 CSS 里的层叠的概念。
为此,您需要构建一个简易的页面标题栏,就像您在某网页顶部看到的那样(如图 1.1 所示)。网站标题位于一组茶色导航链接的上方。最后一个导航链接为橙色,用来代表一个特色链接。
在构建这个网页头部时,您可能已经熟悉了大部分涉及的 CSS 样式。这样一来,就可以重点关注那些一知半解的部分了。
图 1.1 本章要实现的页面标题和导航链接效果
首先,创建一个 HTML 文档和一个名为 styles.css
样式表。将代码清单 1.1 中的内容添加到 HTML 里。
本书的所有代码都可以访问代码库进行下载。该代码库已将所有 CSS 示例样式嵌入对应的若干 HTML 文档内(译注:以 style 标签形式给出,与书中代码略有不同)。
代码清单 1.1 网页头部的 HTML 标记
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<link href="styles.css" rel="stylesheet" type="text/css" />
</head>
<body>
<header class="page-header">
<h1 id="page-title" class="title">Wombat Coffee Roasters</h1> <!-- 网页标题 -->
<nav>
<ul id="main-nav" class="nav"> <!-- 导航链接列表 -->
<li><a href="/coffees">Coffees</a></li>
<li><a href="/brewers">Brewers</a></li>
<li><a href="/specials" class="featured">Specials</a></li> <!-- 特色链接 -->
</ul>
</nav>
</header>
</body>
</html>
当两个或更多规则指向页面上同一元素时,规则间可能包含相互冲突的声明,正如下面的代码所示。三个规则集,每一个都对页面标题指定了不同的字体系列(font family)。该标题不可能同时显示三种字体。哪一个会生效呢?将这些样式代码加到您的 CSS 文件内一探究竟。
代码清单 1.2 相互冲突的样式声明
h1 { /* 标签(或类型)选择器 */
font-family: serif;
}
#page-title { /* ID 选择器 */
font-family: sans-serif;
}
.title { /* 类选择器 */
font-family: monospace;
}
声明冲突的规则集可能会连续出现,也可能分散在样式表的不同位置。无论哪种情况,这些规则集都指向了 HTML 中的同一个元素。
当三个规则集都去设置标题的字体系列,最终生效的是哪一个呢?要回答这个问题,浏览器就必须遵循一系列规则,来确保最终样式是可预测的。本例中,这些判定规则让第二个声明胜出,理由是其选择器中有 id;因此标题最终采用的是无衬线(sans-serif)字体(如图 1.2 所示)。
图 1.2 ID 选择器胜出,标题最终显示为 sans-serif 无衬线字体
层叠指的就是这一系列判定规则。它决定了解决冲突的方式,是 CSS 语言的核心基础。尽管大多数有经验的开发者对层叠的概念大致了解,但其中的部分规则有时还是会引发误解。
让我们来深入剖析层叠规则。当声明发生冲突时,层叠将按照以下顺序、先后通过六个判定标准来消除差异。后续将深入探究每一个判定标准,它们分别是:
- 样式表来源(Stylesheet origin):样式从哪里来,包括您编写的样式和浏览器的默认样式。
- 内联样式(Inline styles):无论是通过 HTML 的 style 属性(attribute),还是通过 CSS 选择器应用到某元素的样式,都在此列。
- 图层(Layer):样式可以在具有不同优先级的每个图层中定义。
- 选择器优先级(Selector specificity):哪些选择器优先于哪些。
- 作用域就近原则(Scope proximity):样式是否只作用于 DOM 的某一部分。
- 源码顺序(Source order):样式在样式表里的声明顺序。
其中一些判定标准受 !important
标注的影响,后续章节会重点讨论。图 1.3 概括地展示了这些规则的用法。
图 1.3 层叠的高级流程图,展示了冲突声明间的先后顺序
有了这些规则,浏览器才能在解决 CSS 冲突时表现出可预测性。在之前的例子中, #page-title
选择器就是基于这些规则而优于其他选择器并最终生效,尤其是选择器优先级的判定标准,很快我将详细阐释。
图层(Layers) 和作用域(scope) 是 CSS 的新增内容,可以更明确地控制层叠。后续第 8 章、第 9 章将深入考察这部分知识。
首先需要强调的是,倘若样式表中没有任何图层,或者任何带作用域的样式,层叠的其余四个判定节点将按此前的惯例继续执行。让我们逐一分析剩下的这些规则:样式表来源、内联样式、选择器优先级和源码顺序。
1.1.1 样式表来源
您添加到网页的样式表并非浏览器呈现样式的唯一来源。
样式表有三种不同的类型或来源。您添加到页面的样式称为 作者样式(author styles);此外还有用户样式(user styles),即终端用户设置的自定义样式;以及用户代理样式(user-agent styles),即浏览器默认样式。用户代理样式的优先级较低,因此会被您的样式所覆盖。而用户样式很少见,即便存在,其优先级也介于用户代理样式和作者样式之间。
用户样式表只能在个别浏览器中定义,或者需要额外安装浏览器扩展程序。由于不常用且往往不受开发者控制,设计页面样式时通常不必担心用户样式表。
用户代理样式在不同浏览器之间略有不同,但通常执行的操作都相同:标题(<h1>
到 <h6>
)和段落(<p>
)会给定一个上、下外边距,列表(<ol>
与 <ul>
)会给到一个左内边距,链接的颜色和默认字体大小也会被设置。
用户代理样式
再来看看示例页面(如图 1.4 所示),标题因为您设置的样式变成了无衬线(sans-serif)字体;其他的样式则交给了浏览器默认设置,比如:列表元素的左侧内边距;由 list-style-type 控制的列表项符号被设为了 disc(小黑点);链接设为蓝色并带下划线;标题和列表都给了上下外边距并且标题的字号还要大一些。
图 1.4 用户代理样式为网页标题设置默认值
浏览器在应用了用户代理样式后才会应用您的样式,即作者样式表。这样您的样式声明才能覆盖用户代理的样式。对于 HTML 关联的多个样式表,其来源都一样,都来自作者样式表。
用户代理样式通常设置的都是用户普遍接受的通用样式,因此大多数情况下不会太突兀。这些默认样式还会在目前主流浏览器的近期版本中保持一致。如果不喜欢,大可在自己的样式表改成想要的效果。说干就干,下面对照图 1.5 将您不需要的默认样式直接覆盖掉:
图 1.5 作者样式因其具有更高的优先级而覆盖了用户代理样式
以下代码已经删除了前面示例中存在冲突的字体系列,并添加了新的样式声明来设置颜色、覆盖用户代理的外边距、列表项的内边距以及圆点项目符号。编辑您的样式表来匹配这些变更吧。
代码清单 1.3 覆盖用户代理样式
h1 {
color: #2f4f4f;
margin-bottom: 10px;
}
#main-nav {
/* 减少外边距 */
margin-top: 10px;
/* 删除列表的用户代理样式 */
list-style: none;
padding-left: 0;
}
#main-nav li {
/* 让列表项水平排列,而非纵向堆叠 */
display: inline-block;
}
#main-nav a {
/* 为导航链接提供类似按钮的外观 */
color: white;
background-color: #13a4a4;
padding: 5px;
border-radius: 2px;
text-decoration: none;
}
如果长期接触 CSS,您大概已经习惯了覆盖用户代理样式。这种做法本质上就是利用了层叠的样式表来源规则。正因为来源不同,您书写的作者样式始终会覆盖掉用户代理样式。
注意
您可能注意到了我在代码中用的是 ID 选择器,但应该避免这样用,原因稍后解释。
重要声明
样式表来源规则还有个例外情况:标记为重要(important) 的声明。在样式声明的末尾、分号的前面加注 !important
字样,该样式就会被标记为重要声明:
color: red !important;
标记了 !important
的声明在来源上享有更高的优先级,因此会始终覆盖任一普通(非重要)来源的样式。汇总整理一下,各类样式可按优先级从大到小降序排列如下:
- 重要用户代理(Important user-agent)
- 重要用户(Important user)
- 重要作者(Important author)
- 普通作者(Normal author)
- 普通用户(Normal user)
- 普通用户代理(Normal user-agent)
上述列表中来源靠前的样式声明,其优先级始终高于靠后的声明。此外还应该注意到,标记为 重要 的几个来源,其优先级顺序刚好与普通来源是反的——到第 8 章讲层叠图层(cascade layers)时我们还会提到样式表来源这一概念。
过渡(Transitions)和关键帧动画(keyframe animations)(后续章节会介绍)还会引入两个额外的样式表来源——这些动画效果为动态变更样式值创建了一套“虚拟”的层叠规则,需要单拎出来特殊对待,以确保样式的可预测性。
层叠规则在解决页面上每个元素、每个属性的样式冲突时,依旧可以保持样式间的独立性。举个例子,给某个段落设置一个加粗的字体,从用户代理样式设置的上下外边距仍然有效,不受字体加粗的影响。!important
注解是 CSS 中一个很奇葩(interesting quirk)的特性,稍后再加以解释。
1.1.2 行内样式
如果无法通过样式表来源规则解决样式冲突,浏览器则会考察它们是否通过行内样式作用于该元素。当使用 HTML 的 style 属性声明样式时,该样式仅对当前元素生效,从而覆盖来自样式表或 <style>
标签的声明。行内样式没有选择器,因为它们直接作用于当前所在元素。
根据示例页头的最终效果,需要将导航菜单中的特色链接(即末尾的 Special 项)设为橙黄色背景,如图 1.6 所示。实现这一效果有很多种方法。先从代码清单 1.4 提供的内联样式开始。
图 1.6 行内样式覆盖了选择器的样式
想查看实测效果,按以下代码修改样式即可(稍后将撤销这部分更改)。
代码清单 1.4 行内样式
<li>
<!-- 通过 style 属性设置行内样式 -->
<a href="/specials" class="featured" style="background-color: orange;">
Specials
</a>
</li>
想要在样式表覆盖掉行内样式,需要给样式加一个 !important
标记,将其提升为优先级更高的样式表来源;但要是行内样式也有 !important
标记,那就彻底没戏了。因此最好只在样式表内使用 !important
。下面撤销代码清单 1.4 中的样式,来看看更好的实现方式。
1.1.3 选择器的优先级
优先级(Specificity) 是 CSS 学习过程中一个容易漏过、但又至关重要的语言特性。不理解样式表来源的概念倒也不妨碍您开发 CSS,因为绝大部分样式都是您自己写的,来源上都属于作者样式;但要是不理解 CSS 的优先级规则,几乎可以肯定是要踩坑的。
如果前面两个规则依旧无法解决样式冲突,浏览器则会进一步考察它们的选择器优先级。譬如,含有两个类名(class names)的选择器,优先级就比仅有一个的高;一个样式声明元素背景为橙黄色,但优先级更高的另一个声明却将其设为茶青色(teal),最终浏览器会用茶青色作背景。
下面举例说明,看看用一个简单的类选择器让特色链接的背景色设为橙黄色是什么效果。更新样式代码如下:
代码清单 1.5 不同优先级的选择器
#main-nav a { /* 更高优先级的选择器 */
color: white;
background-color: #13a4a4; /* 茶青色背景 */
padding: 5px;
border-radius: 2px;
text-decoration: none;
}
.featured { /* 由于优先级较低,橙黄色背景无法覆盖茶青色背景 */
background-color: orange;
}
运行后,橙黄色没有生效!所有导航链接仍然是茶青色。怎么回事?因为此时第一个选择器比第二个更具体(more specific)、优先级更高。它由一个 ID 和一个标签名组成;而第二个仅有一个类名。但仅凭选择器的长度还不足以说明问题。
选择器的类型不同,其优先级也不同。例如,ID 选择器的优先级就比类选择器更高。实际上,单个 ID 的优先级比包含任意多个类的选择器都高。类似地,类选择器的优先级也比含有任意多个标签的标签选择器(即 tag selector,也称为类型选择器,亦即 type selector)的优先级更高。
选择器优先级的具体规则如下:
如若一方选择器 ID 数更多,则更多者胜出(即更具体、更明确);
如若 ID 数量一致,则 类数更多 者胜出;
如若上述比较均一致,则 标签名更多 者胜出。
考察以下代码中的选择器(注意不要添加到您的示例页)。它们是按照优先级从低到高的顺序排列的:
代码清单 1.6 优先级逐渐增高的选择器
html body header h1 { /* 4 个标签 */
color: blue;
}
body header.page-header h1 { /* 3 个标签和 1 个类 */
color: orange;
}
.page-header .title { /* 2 个类 */
color: green;
}
#page-title { /* 1 个 ID */
color: red;
}
描述最具体的选择器是 #page-title
,它有一个 ID,因此标题颜色最终为红色。第二具体的是 .page-header .title
,它有两个类名。如果没有后面的 ID 选择器,则会生效该选择器样式(绿色)。第三个选择器 .page-header .title
比第二个(body header.page-header h1
)优先级更高,尽管长度不及第二个。因为两个类比一个类更具体。最后,第一个选择器 html body header h1
只有四个元素类型(即标签名),没有 ID 或类,因此优先级是最低的。
注意
伪类选择器(Pseudo-class selectors,如 :hover
)以及属性选择器(attribute selectors,如[type="input"]
)的优先级与一个类选择器相同。通用选择器(*
)和组合器(>
、+
、~
)对优先级没有贡献。更多选择器类型信息,请参阅 附录A。
如果添加一个 CSS 样式声明但没有生效,往往是由于被优先级更高的样式覆盖了。许多时候开发人员用到了 ID 选择器,却没考虑过这样会创建更高的优先级,后面再想用其他样式覆盖就很难了。覆盖一个使用了 ID 选择器的样式,就只能用另一个 ID 尝试夺回优先级。
这个概念很简单,但如果不理解优先级,就可能始终都想不明白为什么一个样式能生效,而另一个却不能。
1.1.3.1 优先级的写法
优先级的一个常见写法,是用一组由逗号分隔的数字来表示。例如,1,2,2 表示该选择器包含 1 个 ID、2 个类、2 个标签。优先级最高的 ID 排在首位,其次是类,最后是标签。
选择器 #page-header
#page-title
有 2 个 ID,没有类、也没有标签,其优先级可以写作 2,0,0
;再比如选择器 ul
li
,有 2 个标签,但既没有 ID 也没有类,优先级就是 0,0,2
。表 1.1 给出了代码清单 1.6 中各种选择器的优先级的写法。
表 1.1 各种选择器及其对应的优先级
选择器 | ID | 类(Classes) | 标签(Tags) | 写法(Notation) |
---|---|---|---|---|
html body header h1 |
0 | 0 | 4 | 0,0,4 |
body header.page-header h1 |
0 | 1 | 3 | 0,1,3 |
.page-header .title |
0 | 2 | 0 | 0,2,0 |
#page-title |
1 | 0 | 0 | 1,0,0 |
至此,判定优先级大小的问题就变成了纯数字比较。1,0,0 的优先级要高于 0,2,2,甚至高于 0,10,0(尽管我强烈不推荐写一个含有 10 个类名的选择器),因为第一个数字(代表 ID 数)享有最高的优先级。
业内偶尔也用四位数字来表示优先级。其最高位为 0 或 1,表示该声明是否通过 行内样式 进行指定。因为在 CSS 规范的早期版本中,行内样式最初被定义为优先级的一个子集。这样一来,行内样式的优先级写法就成了 1,0,0,0。这将覆盖通过选择器设置的样式,比如优先级像 0,1,2,0(1 个 ID 和 2 个类)的选择器。
1.1.3.2 关于优先级的思考
之前通过类选择器 .featured
设置橙黄色背景没有奏效,因为有个包含 ID 的选择器 #main-nav a
覆盖了原本的类选择器(优先级分别为 1,0,1
和 0,1,0
)。有很多种方法可以解决这个问题,接下来介绍几种可行方案。
最省事的一个方法是在目标声明中添加 !important
。按以下代码更新样式:
代码清单 1.7 试行方案一
#main-nav a {
color: white;
background-color: #13a4a4;
padding: 5px;
border-radius: 2px;
text-decoration: none;
}
.featured {
/* 标为重要声明,以享有更高优先级来源 */
background-color: orange !important;
}
这次生效了,因为 !important
标记将声明提升到了优先级更高的样式表来源。这个方法的确省事,但也很低级。它可能暂时解决了眼前的问题,但也给您的后续开发挖了坑。一旦给多处声明标注了 !important
,后面再想覆盖掉这些样式又该如何是好?这时浏览器将再次启用样式表来源规则,进而再启用常规的优先级比较规则,最终在绕了一大圈后又让一切回到了原点。
就没有更好的解决方案了吗?与其考虑绕开选择器优先级,不如因势利导为我所用:何不主动提升选择器的优先级呢?将以下代码更新到您的示例文件中:
代码清单 1.8 试行方案二
#main-nav a { /* 优先级仍然为 1,0,1 */
color: white;
background-color: #13a4a4;
padding: 5px;
border-radius: 2px;
text-decoration: none;
}
#main-nav .featured { /* 优先级提升为 1,1,0 */
background-color: orange; /* 此时 !important 标记已经没用了 */
}
这样改也能生效。此时选择器包含 1 个 ID 和 1 个类,优先级为 1,1,0
,比 #main-nav a
(优先级 1,0,1
)更高,所以最终背景变成了橙黄色。
该方法还有改进空间。与其抬升第二个选择器的优先级,不妨试试降低第一个选择器的优先级。注意到该元素包含一个类:<ul id="main-nav" class="nav">
,因此您可以通过类名而非 ID 来选中该元素。按如下代码将选择器 #main-nav
变更为 .nav
。
代码清单 1.9 试行方案三
.nav { /* 将样式表中的 “#main-nav” 全部改为 “.nav” */
margin-top: 10px;
list-style: none;
padding-left: 0;
}
.nav li { /* 将样式表中的 “#main-nav” 全部改为 “.nav” */
display: inline-block;
}
.nav a { /* 第一个选择器的优先级降为 (0,1,1) */
color: white;
background-color: #13a4a4;
padding: 5px;
border-radius: 2px;
text-decoration: none;
}
.nav .featured { /* 第二个选择器的优先级升至 (0,2,0) */
background-color: orange;
}
至此,通过降低选择器的优先级,橙黄色背景的优先级已经足以覆盖掉之前的茶青色背景。
通过这些例子不难发现,优先级的对比容易沦为“军备竞赛”。在大型项目中这一点尤为突出。因此公认的最佳实践是:尽可能让优先级保持低位运行,以便将来需要覆盖样式时,您能有更多回旋的余地。
1.1.4 源码顺序
解决层叠冲突的最后一环叫做 源码顺序,有时又称为 出现顺序(order of appearance)。如果其他判定规则均一致,则样式表中后出现的、或者在页面较晚引入的样式表声明,将最终胜出。
也就是说,可以通过控制源码出现的顺序来给示例中的特色链接添加样式。如果两个存在冲突的选择器优先级也相同 ,则出现较晚的一方将胜出。再来看看下面给出的第四种样式实现方案。这里用到了一个复合选择器 a.featured,中间没有空格。该选择器指向同时满足两个条件的元素:带 featured 类的 <a>
。
代码清单 1.10 试行方案四
.nav a {
color: white;
background-color: #13a4a4;
padding: 5px;
border-radius: 2px;
text-decoration: none;
}
a.featured { /* 优先级变为 (0,1,1) */
background-color: orange;
}
该方案中的两个选择器优先级相等,源码顺序决定了哪个声明实际作用于特色链接,最终获得一个橙黄色背景。
问题看似解决了,但也引入了一个潜在的新问题:如果想在页面其他位置(导航菜单之外)沿用 featured 类,情况又如何呢?如图 1.7 所示,在页面其他位置添加一段内容 <a class="featured">our specials</a>
,会得到一种奇怪的混合样式:背景还是橙黄色,但导航链接的文字颜色、内边距及圆角半径信息都丢失了。
图 1.7 在导航之外使用 featured 类产生的怪异样式
代码清单 1.11 给出了上述效果的 HTML 代码。目标元素仅被第二个选择器命中,而不受第一个影响,因此最终效果未能如愿。是否希望这个橙黄色按钮的样式在导航菜单以外的地方生效,得由您自己来定。如果确实需要,就必须把相关的样式一并带上。
代码清单 1.11 导航菜单外部的特色链接样式
<header class="page-header">
<h1 id="page-title" class="title">Wombat Coffee Roasters</h1>
<nav>
<ul id="main-nav" class="nav">
<li><a href="/coffees">Coffees</a></li>
<li><a href="/brewers">Brewers</a></li>
<li><a href="/specials" class="featured">Specials</a></li>
</ul>
</nav>
</header>
<main>
<p>
Be sure to check out
<!-- nav 外的特色链接只保留了部分样式 -->
<a href="/specials" class="featured">our specials</a>.
</p>
</main>
除非网站有其他需求,否则我更倾向于第三种方法(代码清单 1.9)。理想情况下,您可以对其他位置可能出现的样式需求做出合理的预判,比如确定在别处也会用到这个特色链接,这种情况下,方案四(代码清单 1.10)也许更合适,当然也要注意补全 featured 类缺失的样式。
正如之前所说,在 CSS 中最好的答案通常是“这要看情况”。很多样式的效果都是一题多解,找出不同的实现方法并知晓每种方法的利弊,是非常有价值的。面对同一个样式问题,通常我会分两步来处理:首先找出能达到效果的几个方案,然后再思考其对应的选择器结构,并选出最符合需求的那个方案。
- 链接样式和源码顺序
在您刚开始学习 CSS 时可能就已经了解,要给链接设计样式,书写选择器时就必须遵循特定的顺序。这是因为源码顺序影响了层叠。以下代码给出了链接样式设计的正确打开方式。将它们添加到样式表的开头、nav 样式的前面:
代码清单 1.12 超链接样式
a:link {
background-color: blue;
color: white;
text-decoration: none;
padding: 2px;
}
a:visited {
background-color: purple;
}
a:hover {
background-color: transparent;
color: blue;
text-decoration: underline;
}
a:active {
color: red;
}
层叠才是链接顺序之所以重要的核心本质:在优先级相同的情况下,靠后的样式会覆盖靠前的样式。如果一个元素同时拥有多个状态,则最后那个状态的样式会覆盖其他的状态。比如当用户悬停在已访问的链接上,则悬停样式生效;当用户在悬停状态下激活(activate,即点击它)该链接,激活样式会最终生效。
这个顺序有个不错的记忆口诀:Love/hate (爱/恨法则)—— 即 link(链接)、visited(已访问)、hover(悬停)、active(激活)。注意,如果当中选择器的优先级改得与其他选择器不同,这个规则就会遭到破坏,并得到意想不到的结果。
您还可以用 :any-link
伪类来匹配 :link
或 :visited
状态下的链接。
- 层叠值
浏览器按照上面介绍的这些规则来解析页面上每个元素的每个 property 属性。在层叠规则中胜出的样式声明,称为一个 层叠值。元素的每个属性最多只有一个层叠值。例如页面上一个特定的段落,它可以拥有一上一下两个外边距,但不能同时具有两个不同的上外边距,下外边距也一样。如果 CSS 为同一个属性指定了不同的值,层叠规则最终会选出一个值来渲染元素,这个值就是层叠值。
层叠值:即作为层叠结果最终应用到某元素特定属性上的具体取值。
如果一个元素从未指定某个属性值,则该属性没有层叠值。还是以段落为例,可能就没有特定的边框或内边距。
- 越用越顺手的层叠规则
在处理层叠时曾经有两条通用的经验法则:一是不要在选择器中使用 ID;二是不要使用!important
注解。一旦今后要覆盖这些样式,这两条经验法则似乎都只能帮倒忙。
它们看似没多大用处,但事无绝对。在当前的许多情况下实践这两条经验法则反而反响良好,尤其是在需要考虑图层和作用域的场景下;另一方面也要牢记,切莫为了赢得优先级的“军备之争”而习惯性地套用这两种方法。本书第三部分还将探讨一些控制层叠规则的现代化工具,并展示几个可以打破这些规则的示例。
当创建一个像 NPM 包这样的用于分发的 JavaScript 模块时,强烈建议尽量避免在 JavaScript 里使用行内样式。要是这样做了,那就相当于强迫调用该模块的开发人员站队:要么照单全收里面的所有样式,要么给每个需要修改的 property 属性统一添加 !important
标记。
正确的做法是在包内放一张样式表。如果组件需要频繁修改样式,通常最好是用 JavaScript 给元素添加或移除某个样式类。这样用户就可以放心使用您提供的样式,并根据需要做一些调整,而不用担心优先级带来的竞争问题。
1.2 继承
除了层叠,还有一种给元素设置样式的方式:继承。经常有人把层叠与继承的概念弄混淆。它们虽然有关联,但也应该分辨清楚各自的特点。
如果一个元素的特定属性没有层叠值,则可能会继承某个祖先元素的样式值。在 <body>
元素上设置字体系列 font-family 是很常见的做法。之后其内部的所有元素都将继承该字体,省去了给每个元素手动指定的麻烦。图 1.8 展示了继承是如何沿着 DOM 树向下传播开来的:
图 1.8 被继承的属性从父节点顺着 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.9 所示)。
图 1.9 给 body 设置 font-family
,让其后代元素继承该样式
将如下代码添加到您的示例样式表顶部,让继承的样式生效。
代码清单 1.13 将 font-family
应用到父元素
body {
font-family: sans-serif; /* 被继承的属性值会传递到后代元素上 */
}
这样添加到 body 的样式会在整个页面生效。若将样式指定到特定元素,则该样式只会被其后代元素继承。被继承的样式会逐级传到后代元素,直到被某个层叠值覆盖。
使用开发者工具
被继承与覆盖的属性值在错综复杂的嵌套结构下会很快变得难以追踪。如果您还不熟悉浏览器的开发者工具,请务必养成使用它们的习惯。
DevTools 开发者工具可以精准查看哪些元素应用了哪些样式规则,以及为什么呈现这些样式。层叠和继承都是抽象的概念;而 DevTools 是目前已知最好的样式追踪手段。在一个页面元素上单击鼠标右键,选择弹出菜单中的 检查(Inspect)或 检查元素(Inspect Element)就能打开该工具。如图 1.x 所示。
图 1.x 浏览器的 DevTools 开发工具是查看元素样式的最佳方式
样式检查器显示了目标元素的每个选择器,并按优先级进行排列。选择器样式的下方是继承的所有属性。元素所有的层叠规则与继承样式都一目了然。
还有很多功能细节可以帮助开发人员弄清目标元素当前样式的来龙去脉。靠近顶部的样式会覆盖掉下方的样式。被覆盖的样式会被划掉;右侧给出了每个规则集的样式表名称与行号,以便快速定位到源代码位置。这样就能准确知晓哪个元素继承了哪些样式,以及这些样式的具体来源。此外,还可以在顶部的 Filter 筛选框中筛选出目标样式,同时隐藏其他无关内容。
1.3 特殊值
还有一些特殊的声明值,可以赋给元素的任意属性,以便更好地控制层叠,分别是:inherit、initial、unset 和 revert。下面逐一进行考察。
细心的您可能已经注意到了,代码清单 1.12 给出的超链接样式与平常略有不同,文字颜色由传统的蓝色改为了白色字体、蓝色背景。这么做正是为了演示上述关键字的效果。
1.3.1 inherit 关键字
有时,当继承的样式被层叠值中断后,又想让继承样式重新生效。此时可以使用 inherit 关键字来实现,强制目标元素从父元素继承所需样式。
假设要在示例页添加了一个浅灰色的页脚,页脚内有一些超链接,但不希望它们太显眼,毕竟页脚不是示例页的重点。这就要把页脚内的超链接设置为深灰色(如图 1.10 所示)。
图 1.10 继承灰色文本的使用条款链接
代码清单 1.14 给出了此页脚的 HTML 结构。将其添加到页面末尾,即关闭标签 </body>
的前面。普通网页在页脚和页头之间会显示更多内容,这里方便演示就不展开了。
代码清单 1.14 带链接的页脚
<footer class="footer">
© 2023 Wombat Coffee Roasters —
<a href="/terms-of-use">Terms of use</a>
</footer>
早些时候给示例页上所有的超链接都设置了一些样式,它们也将对《使用条款》的超链接生效。要让页脚内的链接变为灰色,需要用以下代码覆盖原有样式。
代码清单 1.15 inherit 值的应用
.footer {
color: #666; /* 页脚文本设置为灰色 */
background-color: #ccc;
padding: 15px 0;
text-align: center;
font-size: 14px;
}
.footer a {
color: inherit; /* 指定文字颜色从页脚继承 */
background-color: transparent; /* 覆盖蓝色背景为透明 */
text-decoration: underline;
}
上述代码的第二个规则集用一个名为 inherit 的层叠值覆盖了页脚超链接本来的颜色。这样就继承了父元素 <footer>
的颜色。
这么做的好处是,一旦页脚的样式发生任何改动(例如修改第二个规则集,或者被其他样式覆盖),页脚超链接的样式也会同步跟进。例如,页脚文字在某些页面是较深的灰色,其中的超链接也会随之切换为深灰色。
此外,还可以使用 inherit 关键字来强制继承一个通常不会被继承的属性,比如边框或内边距,尽管在实际开发过程中鲜有用武之地。
1.3.2 initial 关键字
有时可能需要撤销某个元素的样式,正如代码清单 1.15 中撤销背景色那样的效果。此时就可以用关键字 initial 来实现目标。
每一个 CSS 属性都有一个初始值或默认值。如果将样式属性赋值为 initial,则可以将其手动设为该属性的默认样式,好比硬复位了该样式。
设置透明背景不要写成 background-color: transparent
,而是使用 initial 值。根据以下代码更新样式。由于 transparent 是背景色属性 background-color 的初始值,最终效果将与上一段代码相同:
代码清单 1.16 initial 值的应用
.footer a {
color: inherit;
background-color: initial; /* 将背景色重置为初始值 */
text-decoration: underline;
}
这样做的好处是不必考虑太多:想移除一个元素的边框,使用 border: initial
即可;想让元素恢复为默认宽度,使用 width: initial
就搞定了。
可能您已经习惯了用 auto 值来实现诸如此类的重置效果;事实上使用 width: auto 确实也行,因为 width 的默认值本就是 auto。
但值得注意的是 auto 并非所有属性的默认值,对许多属性而言,auto 值甚至是无效的。例如 border-width: auto 和 padding: auto 就是无效样式,因此看不到任何效果。您也可以花时间钻研一下这些属性的初始值,但使用 initial 更简单。
注意
样式 display: initial
等价于 display: inline
,并且无论加到什么元素上,都不会产生 display: block
的效果。这是因为 initial 会重置为该属性(property)的初始值,而不是该元素(element)的初始值; inline 才是 display 属性的默认值。在这种情况下,应该考虑使用关键字 revert
。
1.3.3 unset 关键字
关键字 inherit 和 initial 对于清除继承或非继承属性上设置的样式都非常有用。unset 关键字则是二者的结合:用于继承属性时,其值就是 inherit;用于非继承属性时,其值就是 initial。
当然,您也可以分别使用 inherit 和 initial 来实现目标,但是 unset 用起来更简单,可以避免用错关键字。在示例的页脚上,您可以使用 unset 来修复超链接的字体及背景颜色。
代码清单 1.17 unset 值的应用
.footer a {
color: unset; /* 给能被继承的属性一个 inherit 值 */
background-color: unset; /* 给不能被继承的属性一个 initial 值 */
text-decoration: underline;
}
这样,文字颜色就设为了 inherit,使其能够继承页脚的灰色字体;而背景色设为了 transparent,它刚好是 background-color 属性的初始值。
注意:恢复链接的下划线样式仍然需要通过 text-decoration: underline
来实现。此时 unset 并不生效,因为 text-decoration 属性的初始值是 none 而非 underline 。
请务必牢记:CSS 属性(CSS property)本身的初始值始终保持不变;它不受目标元素具体类型的影响。要“撤消”之前设置的 text-decoration 样式,需要用到另外一个关键字。
1.3.4 revert 关键字
关键字 initial 和 unset 基本上可以覆盖包括作者样式和用户代理样式表在内的所有样式。如果只想覆盖此前设置的作者样式,而保留用户代理样式不变。这就是关键字 revert 的用法。
示例页脚中,如果将所有三个属性设为 revert,则超链接的样式将变为带下划线的蓝色文字(即浏览器默认样式);但我们想要的是灰色字体,因此在关键字的选择上应该更仔细:仅将 revert 应用到 text-decoration 属性上。按照代码清单 1.18 更新样式并查看最终效果。
代码清单 1.18 revert 值的应用
.footer a {
color: unset;
background-color: unset;
text-decoration: revert; /* 撤销为用户代理样式 */
}
在本例中,这些关键字可能看起来有些多余,因为这些属性对您而言可能非常熟悉,直接给它们赋上需要的样式值即可;但是,当使用较新的 CSS 功能或遇到像 flexbox 这样、具有多个关键字的样式属性时,可能就没那么容易知晓其对应的默认值了。这时,这些关键字就派上用场了。此外,这么做还有一个额外的好处:可以巧妙地提醒今后的样式开发人员,您这里的意图旨在有效地撤消样式表中其他地方的样式。
这些关键字都是普通的层叠值,意味着当优先级更高的其他选择器也指向同一个元素时,其样式值仍然可以被新样式覆盖。
1.4 简写属性
简写属性(Shorthand properties) 是可以一次性设置多个属性值的样式属性。例如,font
就是一个简写属性。它可以设置多个字体属性:font-style、font-weight、font-size、line-height 以及 font-family:
font: italic bold 18px/1.2 "Helvetica", "Arial", sans-serif;`
其他常见的简写属性还有:
background
:可设置多个背景属性,如:background-color、background-image、background-size、background-repeat、background-position、background-origin、background-clip 以及 background-attachment;
border
:边框样式的简写属性,可以设置:border-width、border-style 以及 border-color,并且这几个属性也都是简写属性;
border-width
:分别是上、右、下、左四个边框宽度的简写属性。
简写属性可以让代码简洁明了,但是也隐藏了一些怪异行为。
1.4.1 当心简写属性悄悄覆盖其他样式
大多数简写属性都可以省略一些值,而只设置我们关心的样式;但重要的是您必须清楚,这么做还是会影响到那些被省略的属性,它们会被隐式地设置为各自的初始值(initial value),从而悄悄覆盖其他地方设计好的样式。例如,在不指定字体粗细(font-weight)的情况下对标题使用简写属性,该标题的字体粗细仍会被赋上一个 normal 值(如图 1.11 所示)。
图 1.11 简写样式 font: 32px sans-serif 将字体粗细和其他省略属性设为了初始值(initial value)
按如下代码更新样式表来验证上述过程。
代码清单 1.19 缩写属性指定所有关联值
h1 {
font-weight: bold;
}
.title {
font: 32px Helvetica, Arial, sans-serif;
}
乍一看, 似乎 <h1 class="title">
会将标题加粗,但并非如此。代码清单 1.19 等价于以下代码:
代码清单 1.20 与代码清单 1.19 等价的展开后的简写属性
h1 {
font-weight: bold;
}
.title {
/* 这些属性的初始值都是 normal */
font-style: normal;
font-variant: normal;
font-weight: normal;
font-stretch: normal;
line-height: normal;
font-size: 32px;
font-family: Helvetica, Arial, sans-serif;
}
这就意味着给 <h1>
元素设置简写样式得到的只是普通粗细的字体,并非粗体字。此外,它还会覆盖从某个祖先元素继承来的其他字体样式。在所有的简写属性中, font 的问题最为严重,因为受其干扰的样式属性实在是太多了。也正是这个原因,除非在 <body>
元素上设置通用样式,我一般都尽量避免用 font 来添加样式。其他简写样式可能也会遇到类似的问题,因此还请务必当心。
1.4.2 记住简写值的顺序
简写属性在指定样式的顺序上没那么严格。设置 border: 1px solid black
或者 border: black 1px solid
都是有效的。这是因为浏览器知道哪个值对应宽度、哪个值对应颜色、哪个值又对应边框样式。
但有不少属性的值是很容易混淆的。在这种情况下,理解并牢记这些样式值的顺序就显得至关重要了,尤其是那些会经常用到的简写属性。
- 上、右、下、左顺序
当遇到像 margin、padding 这样的属性、或者需要指定元素四条边上的边框样式时,开发人员尤其容易弄错这些简写样式的顺序。这些属性值是按顺时针方向、从顶部开始排列的。
记住这个顺序可以少走很多弯路。单词 TRouBLe 可以用作该顺序的记忆口诀:Top(上)、Right(右)、Bottom(下)、Left(左)。
下面就用这个口诀来设置元素各边的内边距。如图 1.12 所示,给导航链接分别指定 10px 的上内边距、15px 的右内边距、0 像素的下内边距、以及 5px 的左内边距。虽然看起来不太匀称,但足以说明这一排序规则:
图 1.12 声明 padding: 10px 15px 0 5px 后每一侧的内边距都不同
相应的 CSS 样式代码如下:
代码清单 1.21 给元素的每一边指定内边距
.nav a {
color: white;
background-color: #13a4a4;
padding: 10px 15px 0 5px; /* 上、右、下、左内边距 */
border-radius: 2px;
text-decoration: none;
}
这种写法还可以再精简。如果声明结束时还有一个值没指定,则没有值的一边会和它对边的取值相同:指定三个值,左右两边用的是第二个值;指定两个值,则上下两边用第一个值,左右两边用第二个值;要是只有一个值,则将其应用到所有四个边。因此,下列声明都是等价的:
padding: 1em 2em;
padding: 1em 2em 1em;
padding: 1em 2em 1em 2em;
同理,以下声明也是等价的:
margin: 1em;
margin: 1em 1em;
margin: 1em 1em 1em;
margin: 1em 1em 1em 1em;
对许多开发人员而言,这当中最棘手的就是只有三个值的情形。请记住,三个值分别指定了上、右、下三个方向。因为左边方向的值缺失了,它的样式值与右侧相同,即左右两边的值皆为第二个值。因此,声明 padding: 10px 15px 0 会给左右两侧 15px 的内边距,与此同时顶部内边距为 10px,底部为 0。
但大多数情况下只需要设置两个值。尤其是像按钮或者本例中的导航链接这样较小的页面元素,左右两边的内边距最好比上下内边距更宽,这样显得更美观一些(如图 1.13 所示)。
图 1.13 很多元素增大水平内边距后会更好看些
按以下代码更新样式表。它利用简写属性先设置了垂直方向上的内边距,然后再设置水平方向的内边距。
代码清单 1.22 指定两个内边距
.nav a {
color: white;
background-color: #13a4a4;
padding: 5px 15px; /* 先是上下内边距,再是左右内边距 */
border-radius: 2px;
text-decoration: none;
}
由于很多常见属性都遵循这个顺序,所以最好记住它。
- 先水平、再垂直的顺序
TRouBLe 口诀仅适用于给盒子四周设置简写属性的情形。其他一些属性最多只支持两个值,比如:background-position
、box-shadow
和text-shadow
(严格来讲它们并不算简写属性)。与padding
这样需要四个值的属性相比,它们的属性值顺序恰巧是相反的。
padding: 1em 2em
先指定垂直方向上的上/下属性值,再是水平方向的左/右属性值;而 background-position: 25% 75%
则先指定水平方向的属性值,然后才是垂直方向的值。
尽管这个看似相反的顺序有些违背人的直觉,但道理很简单:这两个值代表了一个笛卡尔网格(即平面直角坐标系)。笛卡尔网格的测量结果常按 x、y 的顺序给出(先水平再垂直)。打个比方,想要给如图 1.14 所示的元素添加一个阴影效果,就得首先指定 x(水平)值。
图 1.14 盒子的阴影位置为 10px 2px
该元素对应的样式代码如下:
代码清单 1.23 box-shadow 先指定 x 值再指定 y 值
.nav .featured {
background-color: orange;
box-shadow: 10px 2px #6f9090; /* 阴影向右偏移 10px、向下偏移 2px */
}
第一个(较大的)值指定了水平偏移量,第二个(较小的)值指定了垂直偏移量。
如果属性需要指定相对某个位置出发的两个方向上的值,就多想想“笛卡尔网格”;如果属性需要指定元素四周环绕的每个方向上的值,则多想想“时钟”。
1.5 渐进式增强
要用好 CSS 这样一门不断发展演进中的语言,其中一个重要的因素就是要与时俱进,及时了解哪些功能是新鲜出炉的,尤其是那些仅有部分常见浏览器支持的新功能,提前了解是很有必要的。
CSS 同时支持向前与向后兼容的精心设计,让样式代码能够同时在旧新两大类浏览器中流畅运行。只需稍加考虑,这些前沿特性就能在您的 CSS 中得以完美呈现,即便它们尚未对所有用户开放。
为此,可以给旧版浏览器设计一套可接受(但功能较少)的 CSS 样式;而把那些只能在最新版浏览器中呈现的新特性放到新的 CSS 图层(layer)上。这么一来您的样式代码就是面向未来的,随着升级浏览器的用户越来越多,这些新功能新特性也将逐步登台亮相。这种实现方案就叫做 渐进式增强(progressive enhancement)。
想知道一项特定功能的浏览器版本支持情况,请查看 Can I Use 或 MDN 相关文档 。
由于在互联网早期浏览器更新迭代的速度较慢,一些开发人员就总认为那些全新的 CSS 特性几年内都难登大雅之堂。但时至今日,所有主流浏览器都是基业常青的,其新版本的更新发布不仅频繁,而且往往是自动进行的。一些新功能仅仅只需数月就能将浏览器的支持率从 0% 拉升至 80%。之后便会因为用户延迟更新或者囿于公司政策而使更新速度放缓。但无论如何,只要没有明确需求要支持旧版浏览器,新特性通常都会在得到主流浏览器支持后的一两年内稳定下来。
利用适当的渐进式增强配置,您甚至可以即刻小试牛刀,无需恭候多时——尽管也得看该功能的具体适配情况,以及您希望尝试新特性的迫切程度。
1.5.1 利用层叠规则实现渐进式增强
启用渐进式增强最简单的办法是将其构建到层叠规则之中。当浏览器遇到无法解析的样式声明,会直接忽略。考察如下样式代码:
aside {
background-color: #333333; /* 设置一个既安全又普遍支持的十六进制颜色值 */
background-color: #333333aa; /* 使用更新的十六进制编码格式覆盖上一个值 */
}
因为第二个样式声明出现在第一个之后,利用前面介绍的层叠规则,就能断定这些元素的背景色层叠值最终花落谁家。
第二个声明使用了一个相对较新的八位的十六进制颜色编码格式(第 7 位和第 8 位数指定了一个 alpha 通道值,表示部分透明)。该语法在大多数浏览器中均有效,仅有部分旧版浏览器不支持,例如 Internet Explorer 浏览器。因此,如果用户恰巧用的是 IE 浏览器,该样式就会被忽略,层叠值为第一个样式声明的值。尽管该用户无法获得带透明效果的完整体验,但至少还能看到一个完全可用的版本。当前页面不会因此而“崩溃”或抛出错误;浏览器也会继续解析其余 CSS 并丢弃未能识别的样式声明。
从调试的角度来看,这样的处理似乎有些奇特,因为 CSS 从不抛出任何错误。但这恰恰是 CSS 正常运行的一个重要组成部分,也是为了实现渐进式增强的最终目标而精心设计的。
1.5.2 渐进式增强的选择器
渐进式增强的实施并不仅限于新的属性或新的样式值语法层面,还可以体现在新的选择器上。浏览器如果支持该语法就能正常渲染,否则将忽略整个规则集。
还有一个重要细节需要特别注意:当规则集有多个选择器时,只要任何一个选择器不被支持或无效,浏览器渲染时将忽略整个规则集。例如以下样式代码:
input.invalid,
input:user-invalid {
border: 1px solid red;
}
本书撰稿时,伪类 :user-invalid
还是 CSS 的一个新增特性(更多详细信息,请参阅 附录A。落后几个版本的浏览器或许理解 input.invalid
的含义,但由于无法理解 :user-invalid
,即使第一个选择器匹配成功,这些样式也同样不会生效。
要启用这样的新选择器,最好的办法是将它们分开书写:
input.invalid {
border: 1px solid red;
}
input:user-invalid {
border: 1px solid red;
}
这将不可避免地引入冗余样式,但也是当下最好的解决方案了。在使用新的伪类、伪元素或者属性选择器时,请务必牢记这一点(请参阅 附录A)。
遇到上面列举的几个简单情况,尚且只需要重复几行样式代码;但偶尔也会遇到需要重复大段 CSS 的情况。这种情况下,一些开发人员为了尽量避免样式冗余很可能会选择一直等下去,直到浏览器对该选择器的支持情况令人满意后,才会谨慎启用新的功能特性。
1.5.3 利用 @supports()
实现特性查询
前面提到的分而治之的方法,足以应对因启用新特性而对现有样式的影响较小的情况;但偶尔也会遇到给支持新特性的浏览器定制多套不同样式声明的复杂情况。此时就可以利用 CSS 的 特性查询(feature query) 技术,根据浏览器是否支持某个功能特性来定制化开发更大规模的样式效果。
特性查询的写法类似:
@supports (display: grid) { … }
注意,@supports 规则后面有一个带括号的样式声明。如果浏览器能识别该声明(本例即为网格布局),则后面大括号内的所有规则集都会生效,否则予以忽略。
也就是说,您可以提供一套旧的样式布局(如浮动布局)留待备用。这些样式未必是最理想的方案,甚至不得不做出一些让步,但足以满足实际需求。然后再利用特性查询,让完整版的网格布局样式在页面生效。
网格布局方案目前已在所有现代浏览器中得到广泛支持,但以前却并非如此。用它来演示 @supports 的用法再好不过。如图 1.15 所示,在示例页面添加一组超链接,并将其布局到一个小型网格中:
图 1.15 定义在 @supports 代码块内的一组链接网格
首先,将如下 HTML 代码添加到页面标题和页脚之间的 <main>
元素中:
代码清单 1.24 添加到页面的一组超链接
<p>Try some of our newest coffees:</p>
<div class="coffees">
<a href="/coffees/costa-rica">Costa Rica</a>
<a href="/coffees/ethiopia">Ethiopia</a>
<a href="/coffees/guatamala">Guatemala</a>
<a href="/coffees/kenya">Kenya</a>
<a href="/coffees/mexico">Mexico</a>
</div>
接着添加样式,将其设置为网格布局。首先,给旧版浏览器提供一套回退样式;然后利用特性查询,再提供一套完整功能的网格布局版本。代码如下:
代码清单 1.25 利用功能查询来实现渐进式增强
.coffees {
margin: 20px 0;
}
.coffees a {
/* 为旧版浏览器提供一套回退样式 */
display: inline-block;
min-width: 300px;
padding: 10px 15px;
margin-right: 10px;
margin-bottom: 10px;
color: black;
background-color: transparent;
border: 1px solid gray;
border-radius: 5px;
}
@supports (display: grid) { /* 仅对能识别网格布局的浏览器生效如下样式 */
.coffees {
/* 为现代浏览器定义网格布局 */
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
}
.coffees a {
/* 覆盖干扰当前网格布局的回退样式 */
margin: unset;
min-width: unset;
}
}
上述代码中,备用方案和其他基本样式(如颜色)位于特性查询代码块外,因此将对所有浏览器生效。在不支持网格布局的浏览器中打开示例页面,会看到类似网格布局的备用样式效果;而所有基于网格布局的相关样式都在特性查询代码块内,因此仅在浏览器支持网格布局时才会生效。
本书第 5 章还会进一步深入考察网格布局,如果对上述代码不熟悉也无须担心。
试想一下要是没有 @supports 代码块,最终样式会有何不同;甚至可以临时注释掉里面的样式,在现代浏览器中查看样式回退的效果,再酌情调整。
@supports 规则可用于查询各种 CSS 特性的支持情况:
使用 @supports (mix-blend-mode: overlay)
可以查询混合模式(blend mode)的支持情况(详见本书第 11 章);
使用 @supports (color: color-mix(in oklab, red, white))
还能查询 color-mix
特性的支持情况(详见本书第 13 章)。
CSS 特性查询还支持以下几种写法:
@supports not(<declaration>)
:仅当目标声明不受支持时,生效特性查询块中的特定样式规则;
@supports (<declaration>) or (<declaration>)
:支持任一目标声明,则生效对应的样式规则;
@supports (<declaration>) and (<declaration>)
:同时支持两个目标声明,则生效相应的样式规则;
@supports selector(<selector>)
:仅当浏览器能识别目标选择器时,生效相应的样式规则(例如 @supports selector(:user-invalid)
)
有了渐进式增强的解决方案,您可以实现跨浏览器为所有用户提供定制化的用户体验,甚至包括今后的浏览器。一个新的 CSS 语法特性,放到能识别的浏览器内就能渲染,无法识别的也自然不会渲染。
Web 设计师兼教育家 Jen Simmons 半开玩笑地称它为“量子 CSS”(Quantum CSS):对于一个 CSS 特性,“可以同时拥有启用和不启用、生效和不生效两种状态”(“use it and not use it at the same time. It works and it doesn’t work at the same time”)。
CSS 的这种语言特性又被称为 CSS 的 弹性机制(resilience)。CSS 这门语言(以及类似的 HTML 语言)是高容错设计理念的产物。随着将来 CSS 新功能的不断涌现,您可以充分利用它来扩充自己的工具箱。
启用浏览器实验特性
W3C 联盟与浏览器厂商一道共同制定并开发了 CSS 规范。这也意味着在 CSS 规范最终确定之前,一些浏览器可能就已经开始支持某项特性功能的研发了。为了防止正处在生产环境中的网站贸然引入尚待稳定的 CSS 样式效果,这些实验性功能仅对有意在浏览器设置中启用它们的开发人员开放。这样也有利于在规范最终确定前进行早期实验和效果反馈。如果您也想接触这些实验性功能,知道如何开启它们也是至关重要的。
在 Chrome 和 Opera 中,可以通过启用一个标志开关(flag)来完成该操作。在 Chrome 的地址栏内输入 chrome://flags
并按 Enter 键跳转;Opera 浏览器则跳转至 opera://flags
。然后下翻或搜索找到“实验性网络平台功能”(Experimental Web Platform Features),并单击启用该选项。
如果习惯用 Firefox 浏览器,则需要下载并安装 Firefox 开发者版本或 Firefox Nightly 版本。如果习惯用 Safari 浏览器,则需要安装 Safari 技术预览版(Safari Technology Preview)或 Webkit Nightly Builds 版本。
本章小结
浏览器遵循层叠规则来确定哪些样式在哪些元素上生效;
选择器优先级由选择器中的 id 数、class 类的个数以及标签名的个数来共同确定。优先级更高的声明将覆盖较低声明;
当某些属性没有层叠值时,它们会从父元素继承一个样式值。这些属性包括文本、列表和表边框相关的属性;unset 会撤销其他为该属性设置的样式,包括用户代理样式;而 revert 只撤销作者样式,保留用户代理样式;
简写属性提供了一种简洁的方式,可以一次设置多个相关属性的值;对于简写属性,如 margin 和 padding,须按顺时针方向从顶部开始指定属性值;
渐进式增强方案能够在不破坏浏览器原有页面的情况下引入最新的 CSS 特性。