深入解析CSS
层叠、优先级和继承
层叠
层叠指的就是这一系列规则。
它决定了如何解决css样式规则冲突,是 CSS 语言的基础。
虽然有经验的开发人员对层叠有大体的了解,但是层叠里有些规则还是容易让人误解。
当声明冲突时,层叠会依据三种条件解决冲突:
- 样式表的来源:样式是从哪里来的,包括你的样式和浏览器默认样式等。
- 选择器优先级:哪些选择器比另一些选择器更重要。
- 源码顺序:样式在样式表里的声明顺序。
下图概括展示了规则的用法:
这些规则让浏览器以可预测的方式解决 CSS 样式规则的冲突。
样式表的来源
样式的来源主要分为2中:
- 书写的样式表,即作者样式表;
- 用户代理样式表,即浏览器默认样式;
用户代理样式表优先级低,你的样式会覆盖它们。
!important 声明
样式来源规则有一个例外:标记为重要( important)的声明。 如下所示, 在声明的后面、分号的前面加上!important,该声明就会被标记为重要的声明。
color: red !important;
标记了!important 的声明会被当作更高优先级的来源, 因此总体的优先级按照由高到低排列如下所示:
- 作者的!important
- 作者样式表
- 用户代理样式表
理解优先级
如果无法用来源解决冲突声明,浏览器会尝试检查它们的优先级。
理解优先级很重要。
不理解样式的来源照样可以写 CSS,因为 99%的网站样式是来自同样的源。但是如果不理解优先级,就会被坑得很惨。不幸的是,很少有人提及这个概念。
浏览器将优先级分为两部分: HTML 的行内样式和选择器的样式。
行内样式
如果用 HTML 的 style 属性写样式,这个声明只会作用于当前元素。实际上行内元素属于“带作用域的”声明,它会覆盖任何来自样式表或者<style>标签的样式。
行内样式没有选择器,因为它们直接作用于所在的元素。
为了在样式表里覆盖行内声明,需要为声明添加!important,这样能将它提升到一个更高优先级的来源。
但如果行内样式也被标记为!important,就无法覆盖它了。
最好是只在样式表内用!important。
选择器优先级
不同类型的选择器有不同的优先级。比如, ID 选择器比类选择器优先级更高。
实际上, ID选择器的优先级比拥有任意多个类的选择器都高。同理,类选择器的优先级比标签选择器(也称类型选择器)更高。
优先级的准确规则如下:
- 如果选择器的 ID 数量更多,则它会胜出(即它更明确)。
- 如果 ID 数量一致,那么拥有最多类的选择器胜出。
- 如果以上两次比较都一致,那么拥有最多标签名的选择器胜出。
说明:
伪类选择器(如:hover)和属性选择器(如[type="input"])与一个类选择器的优先级相同。
*通用选择器( )和组合器( >、 +、 ~)对优先级没有影响。
如果你在 CSS 里写了一个声明,但是没有生效,一般是因为被更高优先级的规则覆盖了。
很多时候开发人员使用 ID 选择器,却不知道它会创建更高的优先级,之后就很难覆盖它。
如果要覆盖一个 ID 选择器的样式,就必须要用另一个 ID 选择器。
优先级标记
一个常用的表示优先级的方式是用数值形式来标记,通常用逗号隔开每个数。
比如,“1,2,2”表示选择器由 1 个 ID、 2 个类、 2 个标签组成。优先级最高的 ID 列为第一位,紧接着是类,最后是标签。
选择器 | ID | 类 | 标签 | 标记 |
---|---|---|---|---|
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 |
有时,人们还会用 4 个数的标记,其中将最重要的位置用 0 或 1 来表示,代表一个声明是否是用行内样式添加的。此时,行内样式的优先级为“1,0,0,0”。它会覆盖通过选择器添加的样式,比如优先级为“0,1,2,0”( 1 个 ID 和 2 个类)的选择器。
关于优先级的思考
优先级容易发展为一种“军备竞赛”。在大型项目中这一点尤为突出。
通常最好让优先级尽可能低,这样当需要覆盖一些样式时,才能有选择空间。
源码顺序
层叠的第三步,也是最后一步,是源码顺序。
如果两个声明的来源和优先级相同,其中一个声明在样式表中出现较晚,或者位于页面较晚引入的样式表中,则该声明胜出。
链接样式和源码顺序
你刚开始学习 CSS 时,或许就知道给链接加样式要按照一定的顺序书写选择器。这是因为源码顺序影响了层叠。
a:link {
color: blue;
text-decoration: none;
}
a:visited {
color: purple;
}
a:hover {
text-decoration: underline;
}
a:active {
color: red;
}
书写顺序之所以很重要,是因为层叠。
优先级相同时,后出现的样式会覆盖先出现的样式。
如果一个元素同时处于两个或者更多状态,最后一个状态就能覆盖其他状态。如果用户将鼠标悬停在一个访问过的链接上,悬停效果会生效。如果用户在鼠标悬停时激活了链接(即点击了它),激活的样式会生效。
层叠值
层叠值——作为层叠结果,应用到一个元素上的特定属性的值。
浏览器遵循三个步骤,即来源、优先级、源码顺序,来解析网页上每个元素的每个属性。如果一个声明在层叠中“胜出”,它就被称作一个层叠值。
元素的每个属性最多只有一个层叠值。
两条经验法则
处理层叠时有两条通用的经验法则。因为它们很有用:
在选择器中不要使用 ID。
就算只用一个 ID,也会大幅提升优先级。当需要覆盖这个选择器时,通常找不到另一个有意义的 ID,于是就会复制原来的选择器,然后加上另一个类,让它区别于想要覆盖的选择器。
不要使用!important。
它比 ID 更难覆盖,一旦用了它,想要覆盖原先的声明,就需要再加上一个!important,而且依然要处理优先级的问题。
这两条规则是很好的建议,但不必固守它们,因为也有例外。
继承
还有最后一种给元素添加样式的方式: 继承。
经常有人会把层叠跟继承混淆。
虽然两者相关,但是应该分别理解它们。
如果一个元素的某个属性没有层叠值,则可能会继承某个祖先元素的值。
比如通常会给<bod\y>元素加上 font-family,里面的所有后代元素都会继承这个字体,就不必给页面的每个元素明确指定字体了。
但不是所有的属性都能被继承。
默认情况下,只有特定的一些属性能被继承,通常是我们希望被继承的那些。它们主要是跟文本相关的属性: 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>的边框传递到每一个后代元素。)以上为不完全枚举,但是已经很详尽了。
特殊值
有两个特殊值可以赋给任意属性,用于控制层叠: inherit 和 initial。
使用inherit关键字
有时,我们想用继承代替一个层叠值。这时候可以用 inherit 关键字。
可以用它来覆盖另一个值,这样该元素就会继承其父元素的值。
还可以使用 inherit 关键字强制继承一个通常不会被继承的属性,比如边框和内边距。
通常在实践中很少这么做,但是盒模型时,你会看到一个实际用例。
使用initial关键字
有时,你需要撤销作用于某个元素的样式。这可以用 initial 关键字来实现。
每一个 CSS属性都有初始(默认)值。
如果将 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。
简写属性会默默覆盖其他样式
大多数简写属性可以省略一些值,只指定我们关注的值。
但是要知道,这样做仍然会设置省略的值,即它们会被隐式地设置为初始值。这会默默覆盖在其他地方定义的样式。
比如,如果给网页标题使用简写属性 font 时,省略 font-weight,那么字体粗细就会被设置为 normal。
理解简写值的顺序
简写属性会尽量包容指定的属性值的顺序。
可以设置 border: 1px solid black 或者border: black 1px solid,两者都会生效。
这是因为浏览器知道宽度、颜色、边框样式分别对应什么类型的值。
但是有很多属性的值很模糊。
在这种情况下,值的顺序很关键。理解这些简写属性的顺序很重要。
上、右、下、左
当遇到像 margin、 padding 这样的属性,还有为元素的四条边分别指定值的边框属性时,开发者容易弄错这些简写属性的顺序。这些属性的值是按顺时针方向,从上边开始的。
记住顺序能少犯错误。它的记忆口诀是 TRouBLe: top(上)、 right(右)、 bottom(下)、 left(左)。
水平、垂直
还有一些属性只支持最多指定两个值,这些属性包括 background-position、 box-shadow、 text-shadow(虽然严格来讲它们并不是简写属性)。
这些属性值的顺序跟 padding 这种四值属性的顺序刚好相反。比如, padding: 1em 2em 先指定了垂直方向的上/下属性值,然后才是水平方向的右/左属性值,而 background-position: 25% 75%则先指定水平方向的右/左属性值,然后才是垂直方向的上/下属性值。
虽然看起来顺序相反的定义违背了直觉,原因却很简单:这两个值代表了一个笛卡儿网格。笛卡儿网格的测量值一般是按照 x, y(水平,垂直)的顺序来的。
技巧:如果属性需要指定从一个点出发的两个方向的值,就想想“笛卡儿网格”。
如果属性需要指定一个元素四个方向的值,就想想“时钟”。
相对单位
说起给属性指定值, CSS 提供了很多选项。人们最熟悉同时也最简单的应该是像素单位( px)。它是绝对单位,即 5px 放在哪里都一样大。而其他单位,如 em 和 rem,就不是绝对单位,而是相对单位。相对单位的值会根据外部因素发生变化。
相对值的好处
在早期的计算机应用开发程序(以及传统的出版行业)中,开发人员(或者出版商)明确知道其媒介的限制。
一个典型的程序窗口可能宽 400px、高 300px,一个页面可能是宽 4 英寸 ①、高 6.5 英寸。因此,当开发人员设置应用程序的按钮和文字布局时,他们能精确地知道元素在屏幕上的大小和留给其他元素的空间。
在网页上,一切都变了。
那些年追求的像素级完美
在 Web 环境下,用户可以设置浏览器窗口的大小,而 CSS 必须适应这种窗口大小。
此外,当网页打开后,用户还可以缩放网页, CSS 还需要适应新的限制。
也就是说,不能在刚创建网页时就应用样式,而是等到要将网页渲染到屏幕上时,才能去计算样式。
这给 CSS 增加了一个抽象层。
我们无法根据理想的条件给元素添加样式,而是要设置无论元素处于任意条件,都能够生效的规则。
现在的 Web 环境下,网页需要既可以在 4 英寸的手机屏幕上渲染,也可以在 30 英寸的大屏幕上渲染。
在很长时间里,网页设计者通过聚焦到“像素级完美”的设计来降低这种复杂性。
他们会创建一个紧凑的容器,通常是居中的一栏,大约 800px 宽。
然后再像之前的本地应用程序或者印刷出版物那样,在这些限制里面进行设计。
像素级完美的时代终结了
随着技术的发展,加上制造商推出高清显示器,像素级完美的方式逐渐走向了终点。
在 21世纪初,很多人开始讨论是否可以安全地将网页宽度设计成 1024px,而不是 800px。随后,人们又开始讨论同样的话题,是否要将网页宽度设计成 1280px。当时我们得做出选择,到底是让网页宽于旧计算机,还是窄于新计算机。
等到智能手机出现后,开发人员再也无法假装每个用户访问网站的体验都能一样。
不管我们喜欢与否,都得抛弃以前那种固定宽度的栏目设计,开始考虑响应式设计。我们无法逃避 CSS带来的抽象性。我们得拥抱它。
说明:
响应式——在 CSS 中指的是样式能够根据浏览器窗口的大小有不同的“响应”。
这要求有意地考虑任何尺寸的手机、平板设备,或者桌面屏幕。第 8 章会详细介绍响应式设计,但本章会先普及一些重要的基础知识。
CSS 带来的抽象性也带来了额外的复杂性。
如果给一个元素设置 800px 的宽度,在小窗口下会是什么样?水平菜单如果无法在一行显示会是什么样?在写 CSS 的时候,我们既要考虑整体性,也要考虑差异性。当有很多方法解决同一个问题时,我们要选择能够兼顾更多情况的方法。
相对单位就是 CSS 用来解决这种抽象的一种工具。
我们可以基于窗口大小来等比例地缩放字号,而不是固定为 14px,或者将网页上的任何元素的大小都相对于基础字号来设置,然后只用改一行代码就能缩放整个网页。
说明:
像素、点、派卡
CSS 支持几种绝对长度单位,最常用、最基础的是像素( px)。不常用的绝对单位是 mm(毫米)、 cm(厘米)、 in(英寸)、 pt(点,印刷术语, 1/72 英寸)、 pc(派卡,印刷术语,12 点)。这些单位都可以通过公式互相换算: 1in = 25.4mm = 2.54cm = 6pc = 72pt = 96px。因此,16px 等于 12pt( 16/96×72)。设计师经常用点作为单位,开发人员则习惯用像素。因此跟设计师沟通的时候需要做一些换算。
像素是一个具有误导性的名称, CSS 像素并不严格等于显示器的像素,尤其在高清屏(视网膜屏)下。尽管 CSS 单位会根据浏览器、操作系统或者硬件适当缩放,在某些设备或者用户的分辨率设置下也会发生变化,但是 96px 通常等于一个物理英寸的大小。
em 和 rem
em 是最常见的相对长度单位,适合基于特定的字号进行排版。
在 CSS 中, 1em 等于当前元素的字号,其准确值取决于作用的元素。
如下代码:规则集指定了字号为 16px,也就是元素局部定义的 1em。然后使用 em 指定了元素的内边距。
.padded {
font-size: 16px;
padding: 1em;
}
这里设置内边距的值为 1em。浏览器将其乘以字号,最终渲染为 16px。
这一点很重要:浏览器会根据相对单位的值计算出绝对值,称作计算值( computed value)。
使用 em 定义字号
谈到 font-size 属性时, em 表现得不太一样。
之前提到过,当前元素的字号决定了 em。
但是,如果声明 font-size: 1.2em,会发生什么呢?一个字号当然不能等于自己的 1.2 倍。实际上,这个 font-size 是根据继承的字号来计算的。
对大多数浏览器来说,默认的字号为 16px。准确地说, medium 关键字的值是 16px。
em 同时用于字号和其他属性
现在你已经用 em 定义了字号(基于继承的字号),而且也用 em 定义了其他属性,比如padding 和 border-radius(基于当前元素的字号)。
em 的复杂之处在于同时用它指定一个元素的字号和其他属性。
这时,浏览器必须先计算字号,然后使用这个计算值去算出其余的属性值。这两类属性可以拥有一样的声明值,但是计算值不一样。
字体缩小的问题
当用 em 来指定多重嵌套的元素的字号时,就会产生意外的结果。
为了算出每个元素的准确值,就需要知道继承的字号,如果这个值是在父元素上用 em 定义的,就需要知道父元素的继承值,以此类推,就会沿着 DOM 树一直往上查找。
当使用 em 给列表元素定义字号并且多级嵌套时,这个问题就显现出来了。
绝大部分 Web 开发人员曾遇到过类似于下图的现象。文字缩小了!正是这种问题让开发人员惧怕使用 em。
当列表多级嵌套并且给每一级使用 em (em设置小于1)定义字号时,就会发生文字缩小的现象。
因此当这些列表从其他列表继承字号时, em 就会逐渐缩小字号。
这些例子告诉我们,如果不小心的话, em 就会变得难以驾驭。 em 用在内边距、外边距以及元素大小上很好,但是用在字号上就会很复杂。值得庆幸的是,我们有更好的选择: rem。
使用 rem 设置字号
当浏览器解析 HTML 文档时,会在内存里将页面的所有元素表示为 DOM(文档对象模型)。
它是一个树结构,其中每个元素都由一个节点表示。 <html>元素是顶级(根)节点。
它下面是子节点,<head>和<body>。
再下面是逐级嵌套的后代节点。
在文档中,根节点是所有其他元素的祖先节点。
根节点有一个伪类选择器( :root),可以用来选中它自己。这等价于类型选择器 html,但是:root 的优先级相当于一个类名,而不是一个标签。
rem 是 root em 的缩写。
rem 不是相对于当前元素,而是相对于根元素的单位。不管在文档的什么位置使用 rem, 1.2rem 都会有相同的计算值: 1.2 乘以根元素的字号。
与 em 相比, rem 降低了复杂性。
实际上, rem 结合了 px 和 em 的优点,既保留了相对单位的优势,又简单易用。那是不是应该全用 rem,抛弃其他选择呢?答案是否定的。
在 CSS 里,答案通常是“看情况”。 rem 只是你工具包中的一种工具。掌握 CSS 很重要的一点是学会在适当的场景使用适当的工具。
我一般会用 rem 设置字号,用 px 设置边框,用 em 设置其他大部分属性,尤其是内边距、外边距和圆角(不过我有时用百分比设置容器宽度)。
这样字号是可预测的,同时还能在其他因素改变元素字号时,借助 em 缩放内外边距。用 px定义边框也很好用,尤其是想要一个好看又精致的线时。这些是我在设置各种属性时常用的单位,但它们仅仅是工具,在某些情况下,用其他工具会更好。
提示:拿不准的时候,用 rem 设置字号,用 px 设置边框,用 em 设置其他大部分属性。
停止像素思维
过去几年有一个常见的模式,更准确地说是反模式,就是将网页根元素的字号设置为 0.625em或者 62.5%
html {
font-size: .625em;
}
将浏览器的默认字号 16px 缩小为 10px。
这的确能简化计算:如果设计师希望字号为 14px,那么只需要默默在心里除以 10,写上 1.4rem 就可以了,而且还使用了相对单位。
一开始,这会很方便,但是这样有两个缺点。
第一,我们被迫写很多重复的代码。 10px 对大部分文字来说太小了,所以需要覆盖它,最后就得给段落设置 1.4rem,给侧边栏设置 1.4rem,给导航链接设置 1.4rem,等等。这样一来,代码容易出错的地方更多;当需要修改代码时,要改动的地方更多;样式表的体积也更大。
第二,这种做法的本质还是像素思维。虽然在代码里写的是 1.4rem,但是在心里仍然想着“14像素”。在响应式网页中,需要习惯“模糊”值。 1.2em 到底是多少像素并不重要,重点是它比继承的字号要稍微大一点。如果在屏幕上的效果不理想,就调整它的值,反复试验。这种方式同样适用于像素值。
使用 em 时,很容易陷入沉思:到底计算出来的像素值是多少,尤其是用 em 定义字号时。
你会不停地做乘法和除法来计算 em 的值,直到抓狂。相反,我建议先适应使用 em。如果已经习惯了像素, 那么使用 em 可能需要反复练习,但这一切是值得的。
设置一个合理的默认字号
如果你希望默认字号为 14px,那么不要将默认字体设置为 10px 然后再覆盖一遍,而应该直接将根元素字号设置为想要的值。将想要的值除以继承值(在这种情况下为浏览器默认值)是14/16,等于 0.875。
现在你已经给网页设置了想要的字号,不用在其他地方再指定一遍了。
你只需要相对它去修改其他元素(比如标题)的字号。
构造响应式面板
我们可以根据屏幕尺寸,用媒体查询改变根元素的字号。这样就能够基于不同用户的屏幕尺寸,渲染出不同大小的面板。
视口的相对单位
前面介绍的 em 和 rem 都是相对于 font-size 定义的,但 CSS 里不止有这一种相对单位。
还有相对于浏览器视口定义长度的视口的相对单位。
说明:视口——浏览器窗口里网页可见部分的边框区域。它不包括浏览器的地址栏、工具栏、状态栏。
单位如下:
- vh:视口高度的 1/100。
- vw:视口宽度的 1/100。
- vmin:视口宽、高中较小的一方的 1/100( IE9 中叫 vm,而不是 vmin)。
- vmax:视口宽、高中较大的一方的 1/100(本书写作时IE和Edge均不支持vmax)
使用 vw 定义字号
相对视口单位有一个不起眼的用途,就是设置字号, 但我发现它比用 vh 和 vw 设置元素的宽和高还要实用。
通过vm设置字号,即可以实现响应式策略,而不用再使用媒体查询功能。
如果给一个元素加上 font-size: 2vw 会发生什么?在一个 1200px 的桌面显示器上,计算值为 24px( 1200 的 2%)。在一个 768px 宽的平板上,计算值约为 15px( 768 的 2%)。
这样做的好处在于元素能够在这两种大小之间平滑地过渡,这意味着不会在某个断点突然改变。
当视口大小改变时,元素会逐渐过渡。
不幸的是, 24px 在大屏上来说太大了。更糟糕的是,在 iPhone 6 上会缩小到只有 7.5px。如果能够保留这种缩放的能力,但是让极端情况缓和一些就更棒了。 CSS 的 calc()函数可以提供帮助。
使用 calc()定义字号
calc()函数内可以对两个及其以上的值进行基本运算。当要结合不同单位的值时, calc()特别实用。
它支持的运算包括:加( +)、减( −)、乘( ×)、除( ÷)。
加号和减号两边必须有空白,因此我建议大家养成在每个操作符前后都加上一个空格的习惯,比如 calc(1em + 10px)。
用 calc()结合了 em 和 vw 两种单位。
:root {
font-size: calc(0.5em + 1vw);
}
现在打开网页,慢慢缩放浏览器,字体会平滑地缩放。 0.5em 保证了最小字号, 1vw 则确保了字体会随着视口缩放。这段代码保证基础字号从 iPhone 6 里的 11.75px 一直过渡到 1200px 的浏览器窗口里的 20px。可以按照自己的喜好调整这个值。
我们不用媒体查询就实现了大部分的响应式策略。省掉三四个硬编码的断点,网页上的内容也能根据视口流畅地缩放。
无单位的数值和行高
有些属性允许无单位的值(即一个不指定单位的数)。
支持这种值的属性包括 line-height、z-index、 font-weight( 700 等于 bold, 400 等于 normal,等等)。
任何长度单位(如 px、 em、rem)都可以用无单位的值 0,因为这些情况下单位不影响计算值,即 0px、 0%、 0em 均相等。
line-height 属性比较特殊,它的值既可以有单位也可以无单位。
通常我们应该使用无单位的数值,因为它们继承的方式不一样。
- 无单位:后代元素会继承该数值,后代元素的行高为:后代元素字号x数值;
- 有单位:后代元素会继承该行高计算值;
结果源于继承的一个怪异特性:
当一个元素的值定义为长度( px、 em、 rem,等等)时,子元素会继承它的计算值。当使用 em 等单位定义行高时,它们的值是计算值,传递到了任何继承子元素上。如果子元素有不同的字号,并且继承了 line-height 属性,就会造成意想不到的结果,比如文字重叠。
提示:长度——一种用于测量距离的 CSS 值的正式称谓。它由一个数值和一个单位组成,比如 5px。
长度有两种类型:绝对长度和相对长度。百分比类似于长度,但是严格来讲,它不是长度。
使用无单位的数值时,继承的是声明值,即在每个继承子元素上会重新算它的计算值。
这样得到的结果几乎总是我们想要的。我们可以用一个无单位的数值给 body 设置行高,之后就不用修改了,除非有些地方想要不一样的行高。
自定义属性(即 CSS 变量)
你可以声明一个变量,为它赋一个值,然后在样式表的其他地方引用这个值。
说明:如果刚好用了内置变量功能的 CSS 预处理器,比如 Sass 或者 Less,你可能就不太想用 CSS 变量了。千万别这样。新规范里的 CSS 变量有本质上的区别,它比任何一款预处理器的变量功能都多。因此我倾向于称其为“自定义属性”,而不是变量,以强调它跟预处理器变量的区别。
要定义一个自定义属性,只需要像其他 CSS 属性那样声明即可
:root {
--main-font: Helvetica, Arial, sans-serif;
}
定义了一个名叫--main-font 的变量。
变量名前面必须有两个连字符( --),用来跟 CSS 属性区分,剩下的部分可以随意命名 。
变量必须在一个声明块内声明。
调用函数 var()就能使用该变量。
:root {
--main-font: Helvetica, Arial, sans-serif;
}
p {
font-family: var(--main-font);
}
var()函数接受第二个参数,它指定了备用值。如果第一个参数指定的变量未定义,那么就会使用第二个值。
动态改变自定义属性
在前面的示例中,自定义属性只不过为减少重复代码提供了一种便捷方式,但是它真正的意义在于,自定义属性的声明能够层叠和继承:可以在多个选择器中定义相同的变量,这个变量在网页的不同地方有不同的值。
使用 JavaScript 改变自定义属性
还可以使用 JavaScript 在浏览器中实时访问和修改自定义属性。
var rootElement = document.documentElement;
var styles = getComputedStyle(rootElement); // 获取一个元素的 styles对象
var mainColor = styles.getPropertyValue('--main-bg'); // 获取 styles 对象的--main-bg 值
console.log(String(mainColor).trim());
利用这种技术,就可以用 JavaScript 实时切换网站主题,或者在网页中突出显示某些元素,或者实时改变任意多个元素。只需要几行 JavaScript 代码,就可以进行更改,从而影响网页上的大量元素。
盒模型
在网页上实现元素布局涉及很多技术。
在复杂网站上,可能会用到浮动元素、绝对定位元素以及其他各种大小的元素,甚至也会使用较新的 CSS 特性,比如 Flexbox 或者网格布局。
高级的布局话题基于文档流和盒模型等概念,这些是决定网页元素的大小和位置的基本规则。
本章将构建一个两列布局的网页。你可能很熟悉这个布局,因为它是一个经典的 CSS 入门练习,但是在指导你完成这种布局的过程中,我会强调布局中经常被忽略的一些细节。我们会处理盒模型的一些边缘情况,我也会分享一些有关设置元素大小和对齐方式的经验。另外本章还会处理 CSS 中最让人头疼的两个问题:垂直居中和等高列。
元素宽度的问题
使用 float 和 width 进行两列布局:
<body>
<header>
<h1>Franklin Running Club</h1>
</header>
<div class="container">
<main class="main">
<h2>Come join us!</h2>
<p>
The Franklin Running club meets at 6:00pm every Thursday
at the town square. Runs are three to five miles, at your
own pace.
</p>
</main>
<aside class="sidebar">
<div class="widget"></div>
<div class="widget"></div>
</aside>
</div>
</body>
body {
background-color: #eee;
font-family: Helvetica, Arial, sans-serif;
}
header {
color: #fff;
background-color: #0072b0;
border-radius: .5em;
}
.main {
float: left;
width: 70%;
background-color: #fff;
border-radius: .5em;
}
.sidebar {
float: left;
width: 30%;
padding: 1.5em;
background-color: #fff;
border-radius: .5em;
}
两列并没有并排出现,而是折行显示。
虽然将两列宽度设置为 70%和 30%,但它们总共占据的宽度超过了可用空间的 100%,这是因为盒模型的默认行为。
当给一个元素设置宽或高的时候,指定的是内容的宽或高,所有内边距、边框、外边距都是追加到该宽度上的。
这种行为会让一个宽 300px、内边距 10px、边框 1px 的元素渲染出来宽 322px(宽度加左右内边距再加左右边框)。
如果这些值使用不同的单位,情况就会更复杂。
在以上例子中,侧边栏的宽度等于 30%宽度加上各 1.5em 的左右内边距,主容器的宽度只占 70%。两列宽度加起来等于 100%宽度加上 3em。因为放不下,所以两列便折行显示了。
避免魔术数值
最笨的方法是减少其中一列(比如侧边栏)的宽度。在我的屏幕上,侧边栏改为宽 26%,两列能够并排放下,但是这种方式不可靠。 26%是一个魔术数值( magic number)。它不是一个理想的值,而是通过改样式试出来的值。
在编程中不推荐魔术数值,因为往往难以解释一个魔术数值生效的原因。
如果不理解这个数值是怎么来的,就不会知道在不同的情况下会产生什么样的结果。我的屏幕宽 1440px,在更小的视口下,侧边栏仍然会换行。虽然 CSS 中有时确实需要反复试验,但目的是为了得到更好的样式,而不是为了强行将一个元素填入一个位置。
替代魔术数值的一个方法是让浏览器帮忙计算。在本例中,因为加了内边距,两列的宽度总和超出了 3em,所以可以使用 calc()函数减去这个值,得到刚好 100%的总和。比如设置侧边栏宽度为 calc(30% - 3em)就能刚好并排放下两列,但是还有更好的解决办法。
调整盒模型
刚才遇到的问题说明默认的盒模型并不符合需求。相反,我们需要让指定的宽度包含内边距和边框。在 CSS 中可以使用 box-sizing 属性调整盒模型的行为。
box-sizing 的默认值为 content-box,这意味任何指定的宽或高都只会设置内容盒子的大小。将 box-sizing 设置为 border-box 后, height 和 width 属性会设置内容、内边距以及边框的大小总和,这刚好符合示例的要求。
全局设置border-box
使用通用选择器( *)选中了页面上所有元素,并用两个选择器选中了网页的所有伪元素。
*,
::before,
::after {
box-sizing: border-box;
}
说明:将这段代码放到样式表开头已是普遍做法了。
但是,如果在网页中使用了带样式的第三方组件,就可能会因此破坏其中一些组件的布局,尤其是当第三方组件在开发 CSS 的过程中没有考虑到使用者会修改盒模型时。
因为全局设置border-box 时使用的通用选择器会选中第三方组件内的每个元素,修改盒模型可能会有问题,所以最终需要写另外的样式将组件内的元素恢复为 content-box。
有一种简单点的方式,是利用继承改一下修改盒模型的方式。
:root {
box-sizing: border-box; // 根元素设置为border-box
}
*,
::before,
::after {
box-sizing: inherit; // 告诉其他所有元素和伪元素继承其盒模型
}
盒模型通常不会被继承,但是使用 inherit 关键字可以强制继承。
如下述代码所示,可以在必要时选中第三方组件的顶级容器,将其恢复为 content-box。这样组件的内部元素会继承该盒模型。
现在网站上的每个元素都有一个可预测性更好的盒模型了。
给列之间加上间隔
这里介绍2中方式
第一种方式:给其中一列加上外边距,再调整元素的宽度,将多出来的空间减掉。 如从侧边栏的宽度中减掉了 1%,将其增加到外边距上。
.main {
float: left;
width: 70%;
background-color: #fff;
border-radius: .5em;
}
.sidebar {
float: left;
width: 29%;
margin-left: 1%;
padding: 1.5em;
background-color: #fff;
border-radius: .5em;
}
这段代码的确加上了间隔,但是间隔的宽度由外层容器的宽度决定,百分比是相对于父元素的完整宽度的。如果想用其他单位指定间距呢?(我更想用 em 指定间距,因为 em 单位的一致性更好。)可以用 calc()来实现。
方式二:如可以从宽度中减掉 1.5em 分给外边距,而不是完整宽度的 1%。如下使用用 calc()实现了这种效果
.main {
float: left;
width: 70%;
background-color: #fff;
border-radius: .5em;
}
.sidebar {
float: left;
width: calc(30% - 1.5em);
margin-left: 1.5em;
padding: 1.5em;
background-color: #fff;
border-radius: .5em;
}
元素高度的问题
处理元素高度的方式跟处理宽度不一样。
之前对 border-box 的修改依然适用于高度,而且很有用,但是通常最好避免给元素指定明确的高度。
普通文档流是为限定的宽度和无限的高度设计的。内容会填满视口的宽度,然后在必要的时候折行。因此,容器的高度由内容天然地决定,而不是容器自己决定。
控制溢出行为
当明确设置一个元素的高度时,内容可能会溢出容器。当内容在限定区域放不下,渲染到父元素外面时,就会发生这种现象。
文档流不考虑溢出的情况,其容器下方的任何内容都会渲染到溢出内容的上面。
用 overflow 属性可以控制溢出内容的行为,该属性支持以下 4 个值。
- visible(默认值) ——所有内容可见,即使溢出容器边缘。
- hidden——溢出容器内边距边缘的内容被裁剪,无法看见。
- scroll——容器出现滚动条,用户可以通过滚动查看剩余内容。在一些操作系统上,会出现水平和垂直两种滚动条,即使所有内容都可见(不溢出)。不过,在这种情况下,滚动条不可滚动(置灰)。
- auto——只有内容溢出时容器才会出现滚动条。
通常情况下,我倾向于使用 auto 而不是 scroll,因为在大多数情况下,我不希望滚动条一直出现。
请谨慎地使用滚动条。浏览器给网页最外层加上了滚动条,如果网页内部再嵌套滚动区域,用户就会很反感。
如果用户使用鼠标滚轮滚动网页,当鼠标到达一个较小的滚动区域,滚轮就会停止滚动网页,转而滚动较小的区域。
说明:水平方向的溢出
除了垂直溢出,内容也可能在水平方向溢出。一个典型的场景就是在一个很窄的容器中放一条很长的 URL。溢出的规则跟垂直方向上的一致。
可以用 overflow-x 属性单独控制水平方向的溢出,或者用 overflow-y 控制垂直方向溢出。这些属性支持 overflow 的所有值,然而同时给 x 和 y 指定不同的值,往往会产生难以预料的结果。
百分比高度的备选方案
用百分比指定高度存在问题。
百分比参考的是元素容器块的大小(即包含块的大小,宽度百分比基于包含块的宽度,高度百分比基于包含块的高度),但是容器的高度通常是由子元素的高度决定的。这样会造成死循环,浏览器处理不了,因此它会忽略这个声明。
要想让百分比高度生效,必须给父元素明确定义一个高度。
人们使用百分比高度是想让一个容器填满屏幕。不过更好的方式是用视口的相对单位 vh
还有一个更常见的用法是创造等高列。这不用百分比也能实现。
等高列
等高列的问题从 CSS 出现就一直困扰着人们。
在 21 世纪初, CSS 取代了 HTML 表格成为布局的主要方式。当时表格是实现等高列的唯一方式,更具体地说,是不明确指定高度就能实现等高列的唯一方式,虽然可以简单地将所有列设置高度 500px 或者其他任意值,但是如果要让列自己决定高度,每个元素可能算出来都不一样高,具体高度取决于内容。
为了解决这个问题,诞生了很多有创意的解决方案。随着 CSS 的演进,出现了伪元素、负外边距等方案。如果你还在用这些复杂的方案,那么是时候改变了。
现代浏览器支持了 CSS 表格,可以轻松实现等高列,比如 IE8+支持 display: table, IE10+支持弹性盒子或者 Flexbox,都默认支持等高列。
很多常见的设计需要等高列,比如本章的两列布局就是个典型的例子。
如果将主列和侧边栏的高度对齐,看起来就会更精致。任意一列的内容增加,两列的高度都会增加,同时保持底部对齐。
当然,你可以给两列随便设置一个高度值,但是应该选择什么值呢?太大了就会在容器底部留下大片空白,太小了内容就会溢出。
最好的办法是让它们自己决定高度,然后扩展较矮的列,让它的高度等于较高的列。下面会演示通过 CSS 表格和 Flexbox 两种方式实现这种效果。
CSS 表格布局
首先,用 CSS 表格布局替代浮动布局。给容器设置 display: table,给每一列设置 display: table-cell。
header {
color: #fff;
background-color: #0072b0;
border-radius: .5em;
}
.container {
display: table; // 1. 让容器布局像表格一样
width: 100%; // 让表格填充容器的宽度
}
.main {
display: table-cell; // 让列布局像表格的单元格一样
width: 70%;
background-color: #fff;
border-radius: .5em;
}
.sidebar {
display: table-cell; // 让列布局像表格的单元格一样
width: 30%;
margin-left: 1.5em; // 2. 外边距不再生效
padding: 1.5em;
background-color: #fff;
border-radius: .5em;
}
不像 block 的元素,默认情况下,显示为 table 的元素宽度不会扩展到 100%,因此需要明确指定宽度(编号:1)。
以上代码已经差不多实现了需求,但是缺少间隔。这是因为外边距 并不会作用于 table-cell 元素,所以要修改代码,让间隔生效。
可以用表格元素的 border-spacing 属性来定义单元格的间距。
该属性接受两个长度值:水平间距和垂直间距。(也可以将这两个长度值指定为同一值。)可以给容器加上 border-spacing: 1.5em 0,但这会产生一个特殊的副作用:这个值也会作用于表格的外边缘。这样两列就无法跟头部左右对齐了。如下图:
机智的你可能会想到负外边距,但是这需要给整个表格包裹一层新的容器。
具体步骤:在表格容器外面包一个元素<div class="wrapper">,将其左右外边距设置为-1.5em,从而抵消表格容器外侧 1.5em 的 border-spacing。
.wrapper { // 添加一个新的包裹元素,设置负外边距
margin-left: -1.5em;
margin-right: -1.5em;
}
.container {
display: table;
width: 100%;
border-spacing: 1.5em 0; // 单元格之间加上水平的border-spacing
}
.main {
display: table-cell;
width: 70%;
background-color: #fff;
border-radius: .5em;
}
.sidebar {
display: table-cell;
width: 30%;
padding: 1.5em;
background-color: #fff;
border-radius: .5em;
}
正的外边距会将容器的边缘往里推,而负的外边距则会将边缘往外拉。
结合 border-spacing,两列靠近外侧的边缘跟<body>(包裹元素所在的容器盒子)的边缘对齐了。
现在的布局满足了需求:两列等高, 1.5em 的间距,外边缘跟头部对齐。
如下图:
说明:用表格实现布局
如果你已经做了一段时间 Web 开发,大概听过用 HTML 表格实现布局并非明智之举。在21 世纪初,很多网站设计师使用、<table>元素实现网站布局,因为它比用浮动布局(当时唯一的替代方案)简单。后来,有许多人强烈反对表格布局,因为它用了无语义的 HTML 标签。那时表格没有承担内容标签的功能,反而做着本该由 CSS 负责的布局工作。
浏览器现在支持将各种元素显示为表格,而不只是<table>,因此我们可以一边享受表格布局带来的好处,一边维护语义标记。然而这种方式并不是完美的解决方案, HTML 表格的colspan 和 rowspan 属性在 CSS 中没有可替代的方案,而且浮动、 Flexbox 以及 inline-block可以实现表格无法实现的布局。
Flexbox
我们还可以用 Flexbox 实现两列等高布局, Flexbox 不需要一个额外的 div 包裹元素,它默认会产生等高的元素。
此外也不需要使用负外边距。
.container {
display: flex; // 将容器的 display属性设置为 flex
}
.main {
width: 70%; // 弹性容器内的元素不需要指定display 或者 float 属性
background-color: #fff;
border-radius: 0.5em;
}
.sidebar {
width: 30%; // 弹性容器内的元素不需要指定display 或者 float 属性
padding: 1.5em;
margin-left: 1.5em;
background-color: #fff;
border-radius: .5em;
}
给容器设置 display: flex,它就变成了一个弹性容器( flex container),子元素默认等高。
你可以给子元素设置宽度和外边距,尽管加起来可能超过 100%, Flexbox 也能妥善处理。
以上代码清单渲染出来的样式跟表格布局一样,而且不需要额外包裹元素, CSS 也更简单。
警告:除非别无选择,否则不要明确设置元素的高度。先寻找一个替代方案。设置高度一定会导致更复杂的情况。
使用min-height和max-height
你可以用这两个属性指定最小或最大值,而不是明确定义高度,这样元素就可以在这些界限内自动决定高度。
如果你想要将一张大图放在一大段文字后面,但是担心它溢出容器,就可以用 min-height指定一个最小高度,而不指定它的明确高度。这意味着元素至少等于你指定的高度,如果内容太多,浏览器就会允许元素自己扩展高度,以免内容溢出。
如下图有三个元素。
左边的元素没有 min-height,因此它的高度由自身决定,另外两个元素都设置了 min-height 为 3em。中间的元素如果自己决定高度的话应该比现在矮,但是min-height 值让它的高度为 3em。右边的元素内容多到已经超过 3em,容器自然地扩展高度,以容纳内容。
同理, max-height 允许元素自然地增高到一个特定界限。如果到达这个界限,元素就不再增高,内容会溢出。
还有类似的属性是 min-width 和 max-width,用于限制元素的宽度。
垂直居中内容
CSS 另一个让人头疼的问题就是垂直居中。过去有好几种方式实现垂直居中,但是每一种方式都有一定的局限性。
在 CSS 中,回答一个问题的答案通常是“这得看情况”,垂直居中就是如此。
说明:为什么 vertical-align 不生效
如果开发人员期望给块级元素设置 vertical-align: middle 后,块级元素里的内容就能垂直居中,那么他们通常会失望,因为浏览器会忽略这个声明。
vertical-align 声明只会影响行内元素或者 table-cell 元素。对于行内元素,它控制着该元素跟同一行内其他元素之间的对齐关系。比如,可以用它控制一个行内的图片跟相邻的文字对齐。
对于显示为 table-cell 的元素, vertical-align 控制了内容在单元格内的对齐。如果你的页面用了 CSS 表格布局,那么可以用 vertical-align 来实现垂直居中。
CSS 中最简单的垂直居中方法是给容器相等的上下内边距,让容器和内容自行决定自己的高度。
header {
padding-top: 4em; // 相同的上下内边距,不用指定高度也能让元素内容垂直居中
padding-bottom: 4em;
color: #fff;
background-color: #0072b0;
border-radius: .5em;
}
不管容器里的内容显示为行内、块级或者其他形式,这种方法都有效,但有时我们想给容器设置固定高度,或者无法使用内边距,因为想让容器内另一个子元素靠近容器的顶部或者底部。
不同的情况有不同的处理方法,具体参考如下:
垂直居中指南
在容器里让内容居中最好的方式是根据特定场景考虑不同因素。做出判断前,先逐个询问自己以下几个问题,直到找到合适的解决办法。其中一些技术会在后面的章节中介绍,可根据情况翻阅对应的内容寻找答案。
- 可以用一个自然高度的容器吗? 给容器加上相等的上下内边距让内容居中。
- 容器需要指定高度或者避免使用内边距吗? 对容器使用 display: table-cell 和vertical-align: middle。
- 可以用 Flexbox 吗? 如果不需要支持 IE9,可以用 Flexbox 居中内容。
- 容器里面的内容只有一行文字吗? 设置一个大的行高,让它等于理想的容器高度。这样会让容器高度扩展到能够容纳行高。如果内容不是行内元素,可以设置为 inline-block。
- 容器和内容的高度都知道吗? 将内容绝对定位。参见 定位和层叠上下文。(只有当前面提到的方法都无效时才推荐这种方式。)
- 不知道内部元素的高度? 用绝对定位结合变形( transform)。参见 变换 的例子。(还是只有当前面提到的方法都无效时才推荐该方法。)
还不确定的话,参考 howtocenterincss.com 网站。这个网站很不错,可以根据自己的场景填写几个选项,然后它会相应地生成垂直居中的代码。
负外边距
不同于内边距和边框宽度,外边距可以设置为负值。负外边距有一些特殊用途,比如让元素重叠或者拉伸到比容器还宽。
负外边距的具体行为取决于设置在元素的哪边,如下图。
如果设置左边或顶部的负外边距,元素就会相应地向左或向上移动,导致元素与它前面的元素重叠,如果设置右边或者底部的负外边距,并不会移动元素,而是将它后面的元素拉过来。
给元素底部加上负外边距并不等同于给它下面的元素顶部加上负外边距。
如果不给一个块级元素指定宽度,它会自然地填充容器的宽度。但如果在右边加上负外边距,则会把它拉出容器(元素宽度变长,右边会超出容器)。如果在左边再加上相等的负外边距,元素的两边都会扩展到容器外面(左右两边都会超出容器)。
这就是为什么在 css表格布局 中,使用负外边距忽略 border-spacing 的影响。
警告:如果元素被别的元素遮挡, 利用负外边距让元素重叠的做法可能导致元素不可点击。
外边距折叠
当顶部和/或底部的外边距相邻时,就会重叠,产生单个外边距。这种现象被称作折叠。
如下方法可以防止外边距折叠。
- 对容器使用 overflow: auto(或者非 visible 的值),防止内部元素的外边距跟容器外部的外边距折叠。这种方式副作用最小。
- 在两个外边距之间加上边框或者内边距,防止它们折叠。
- 如果容器为浮动元素、内联块、绝对定位或固定定位时,外边距不会在它外面折叠。
- 当使用 Flexbox 布局时,弹性布局内的元素之间不会发生外边距折叠。网格布局同理。
- 当元素显示为 table-cell 时不具备外边距属性,因此它们不会折叠。此外还有 table-row和大部分其他表格显示类型,但不包括 table、 table-inline、 table-caption。
理解浮动
将介绍最重要的三种改变文档流的方式:浮动、 Flexbox 和网格布局。
此外还会介绍定位,它的主要作用是将元素堆叠到其他元素之上。
浮动的设计初衷
浮动能将一个元素(通常是一张图片)拉到其容器的一侧,这样文档流就能够包围它。
浮动元素会被移出正常文档流,并被拉到容器边缘。
文档流会重新排列,但是它会包围浮动元素此刻所占据的空间。
如果让多个元素向同侧浮动,它们就会挨着排列,如图 4-2 所示
尽管这才是浮动的设计初衷,我们却并不总是这样使用它。
在 CSS 早期,开发人员发现使用简单的浮动就可以移动页面的各个部分,从而实现各种各样的布局。浮动本身不是为了实现页面布局而设计的,但是在近 20 年的时间里,我们把它当成了布局工具。
容器折叠和清除浮动
理解容器折叠
浮动元素不同于普通文档流的元素,它们的高度不会加到父元素上。
元素使用了浮动,容器会出现高度塌陷(容器折叠)的情况。
这可能看起来很奇怪,但是恰好体现了浮动的设计初衷。
浮动的出现是为了实现文字围绕浮动元素排列的效果。在段落里浮动图片时,段落的高度并不会增长到能够容纳该图片。也就是说,如果图片比段落文字高,下一段会直接从上一段的文字下面开始,两段文字都会围绕浮动的图片排列,如图 4-7 所示。
一个解决办法是使用跟浮动配套的 clear 属性。将一个元素放在主容器的末尾,并对它使用 clear,这会让容器扩展到浮动元素下面。
<main class="main">
...
<div style="clear: both"></div> // 在 main 容器的末尾增加一个带有 clear 属性的空 div
</main>
clear: both 声明让该元素移动到浮动元素的下面,而不是侧面。
clear 的值还可以设置为 left 或者 right,这样只会相应地清除向左或者向右浮动的元素。因为空 div 本身没有浮动,所以容器就会扩展,直到包含它,因此也会包含该 div 上面的浮动元素。
这种方法的确能实现预期的行为,但是不雅。要在 HTML 里添加不必要的标记,才能实现本应该由 CSS 实现的效果。因此我们要删掉上面的空 div 标签,用纯 CSS 方案来实现相同的效果。
理解清除浮动
不用额外的 div 标签,我们还可以用伪元素( pseudo-element)来实现。
使用::after 伪元素选择器,就可以快速地在 DOM 中在容器末尾添加一个元素,而不用在 HTML 里添加标记。
说明:伪元素——一种特殊的选择器,可以选中文档的特定部分。伪元素以双冒号( ::)开头,大部分浏览器为了向后兼容也支持单冒号的形式。最常见的伪元素是::before 和::after,用来向元素的开始或者结束位置插入内容。
在包含容器添加一个clearfix类,插入如下样式:
.clearfix::after { // 选中容器末尾的伪元素
display: block; // 将伪元素的 display 设置为非 inline,并给定一个 content 值,以便让伪元素出现在文档中
content: " ";
clear: both; // 让伪元素清除容器中的所有浮动
}
这个清除浮动还有个一致性问题没有解决:浮动元素的外边距不会折叠到清除浮动容器的外部,非浮动元素的外边距则会正常折叠。比如在前面的页面里,标题“Running tips”紧挨着白色的<main>元素的顶部(如图 4-8 所示),它的外边距在容器外面折叠了。
一些开发人员更喜欢使用清除浮动的一个修改版,它能包含所有的外边距,这样更符合预期。使用这个修改版,能防止标题顶部的外边距在 main 元素的外部折叠。
.clearfix::before, // 让::before 和::after伪元素都显示出来
.clearfix::after {
display: table; // 防止伪元素的外边距折叠
content: " ";
}
.clearfix::after { // 只有::after 伪元素需要清除浮动
clear: both;
}
这个版本使用 display: table 而不是 display: block。
给::before 和::after 伪元素都加上这一属性,所有子元素的外边距都会包含在容器的顶部和底部之间。
说明:清除浮动和 display: table
在清除浮动时使用 display: table 能够包含外边距,是因为利用了 CSS 的一些特性。
创建一个 display: table 元素(或者是本例的伪元素),也就在元素内隐式创建了一个表格行和一个单元格。因为外边距无法通过单元格元素折叠(参见 盒模型),所以也无法通过设置了 display: table 的伪元素折叠。
看起来似乎使用 display: table-cell 也能达到相同的效果, 但是 clear 属性只能对块级元素生效。表格是块级元素,但是单元格并不是。因此, clear 属性无法跟 display:table-cell 一起使用。所以要用 display: table 来清除浮动,同时利用隐式创建单元格
来包含外边距。
用什么版本的清除浮动取决于你。
有些开发人员认为外边距折叠是 CSS 里的基础特性,因此他们选择不包含外边距。
不过以上两个版本的清除浮动都没有包含浮动元素的外边距,因此其他人会选择修改版以获得更一致的行为。两种观点都有自己的优势。
出乎意料的“浮动陷阱”
浏览器会将浮动元素尽可能地放在靠上的地方,如图 4-10所示。
因为盒子 2 比盒子 1 矮,所以它下面有多余的空间给盒子 3。盒子 3 会“抓住”盒子 1,而不是清除盒子 1 的浮动。因此盒子 3 不会浮动到最左边,而是浮动到盒子 1 的右下角。
这种行为本质上取决于每个浮动块的高度。即使高度相差 1px,也会导致这个问题。
相反,如果盒子 1 比盒子 2 矮,盒子 3 就没法抓住盒子 1 的边缘。除非以后内容改变导致元素高度发生变化,否则就不会看到这种现象。
众多的元素浮动到同一侧,如果每个浮动盒子的高度不一样,最后的布局可能千变万化。同理,改变浏览器的宽度也会造成相同的结果,因为它会导致换行,从而改变元素高度。而我们真正想要的是每行有两个浮动盒子,如图 4-11 所示。
要想修复这个问题很简单:清除第三个浮动元素上面的浮动(清除上面浮动的造成的影响)。更通用的做法是,清除每行的第一个元素上面的浮动。由于已知每行有两个盒子, 因此只需要清除每行的第奇数个元素上面那行的浮动即可。你可以用:nth-child()伪类选择器选中这些目标元素。
.media {
float: left;
width: 50%;
padding: 1.5em;
background-color: #eee;
border-radius: 0.5em;
}
.media:nth-child(odd) {
clear: left; // 每个新行清除了上面一行的浮动
}
媒体对象和 BFC
BFC
如果在浏览器开发者工具里检查媒体正文(单击鼠标右键,选择检查或者检查元素),就会发现它的盒子扩展到了最左边,因此它会包围浮动的图片(如图 4-14 左边所示)。
现在文字围绕着图片,但是只要清除了图片底部的浮动,正文就会立刻移动到媒体盒子的右边。而我们真正想要的是将正文的左侧靠着浮动图片的右侧排列(如图 4-14 右边所示)。
为了实现右边这种布局,需要为正文建立一个块级格式化上下文( block formatting context, BFC)。
BFC 是网页的一块区域,元素基于这块区域布局。虽然 BFC 本身是环绕文档流的一部分,但它将内部的内容与外部的上下文隔离开。这种隔离为创建 BFC 的元素做出了以下 3 件事情。
- 包含了内部所有元素的上下外边距。它们不会跟 BFC 外面的元素产生外边距折叠。
- 包含了内部所有的浮动元素(防止因为内部元素浮动而产生高度塌陷)。
- 不会跟 BFC 外面的浮动元素重叠。
简而言之, BFC 里的内容不会跟外部的元素重叠或者相互影响。
如果给元素增加 clear 属性,它只会清除自身所在 BFC 内的浮动。
如果强制给一个元素生成一个新的 BFC,它不会跟其他 BFC 重叠。
给元素添加以下的任意属性值都会创建 BFC:
- float: left 或 right,不为 none 即可。
- overflow: hidden、 auto 或 scroll,不为 visible 即可。
- display: inline-block、 table-cell、 table-caption、 flex、 inline-flex、grid 或 inline-grid。拥有这些属性的元素称为块级容器( block container)。
- position: absolute 或 position: fixed。
使用 overflow: auto 通常是创建 BFC 最简单的一种方式。也可以使用前面提到的其他方式,但是有些问题需要注意,比如,使用浮动或者 inline-block 方式创建 BFC 的元素宽度会变成 100%,因此需要限制一下元素的宽度,防止因为过宽而换行,导致内容移动到浮动图片的下面。相反,使用 table-cell 方式显示的元素,其宽度只会刚好容纳其中的内容,因此需要设置一个较大的宽度,强制使其填满剩余空间。
网格系统
现在整个页面的布局已经创建好了,但是还存在一些不足。最主要的问题是,无法轻松地复用样式表中的内容。
如果想要复用前面的设计,但需要一行放三个元素,那又该怎么办呢?
一种比较普遍的做法是借助网格系统提高代码的可复用性。
网格系统提供了一系列的类名,可添加到标记中,将网页的一部分构造成行和列。它应该只给容器设置宽度和定位,不给网页提供视觉样式,比如颜色和边框。需要在每个容器内部添加新的元素来实现想要的视觉样式。
大部分流行的 CSS 框架包含了自己的网格系统。它们的实现细节各不相同,但是设计思想相同:在一个行容器里放置一个或多个列容器。列容器的类决定每列的宽度。接下来构建一个网格系统,这样你就能掌握它的工作原理,进而应用到网页中。
理解网格系统
要构建一个网格系统,首先要定义它的行为。通常网格系统的每行被划分为特定数量的列,一般是 12 个,但也可以是其他数。每行子元素的宽度可能等于 1~12 个列的宽度。
图 4-16 展示了一个 12 列网格中不同的行。第一行有 6 个 1 列宽的子元素和 3 个 2 列宽的子元素。第二行有一个 4 列宽的子元素和一个 8 列宽的子元素。因为每行子元素的宽度加起来都等于 12 列的宽度,所以刚好填满整行。
选取 12 作为列数是因为它能够被 2、 3、 4、 6 整除,组合起来足够灵活。比如可以很容易地实现一个 3 列布局( 3 个 4 列宽的元素)或者一个 4 列布局( 4 个 3 列宽的元素)。还可以实现非对称的布局,比如一个 9 列宽的主元素和一个 3 列宽的侧边栏。在每个子元素里可以放置任意标记。
下面代码里的标记直观地展示了网格系统。每行有一个行容器 div,在其中用 column-n 类为每个列元素放置一个 div( n 是网格里的列数)。
<div class="row">
<div class="column-4">4 column</div>
<div class="column-8">8 column</div>
</div>
FlexBox
flex属性
flex 属性是三个不同大小属性的简写: flex-grow、 flex-shrink 和 flex-basis。
推荐使用简写属性 flex, 而不是分别声明 flex-grow、 flex-shrink、flex-basis。
与大部分简写属性不一样,如果在 flex 中忽略某个子属性,那么子属性的值并不会被置为初始值。
相反,如果某个子属性被省略,那么 flex 简写属性会给出有用的默认值: flex-grow 为 1、 flex-shrink 为 1、 flex-basis 为 0%。这些默认值正是大多数情况下所需要的值。
使用flex-basis属性
flex-basis 定义了元素大小的基准值,即一个初始的“主尺寸”。
flex-basis 属性可以设置为任意的 width 值,包括 px、 em、百分比。
它的初始值是 auto,此时浏览器会检查元素是否设置了 width 属性值。
如果有,则使用 width 的值作为 flex-basis 的值;如果没有,则用元素内容自身的大小。
如果 flex-basis 的值不是 auto, width 属性会被忽略。
注意:在flex简写中,flex-basis属性值初始值为0%,不为auto
使用flex-grow属性
每个弹性子元素的 flex-basis 值计算出来后,它们(加上子元素之间的外边距)加起来会占据一定的宽度。加起来的宽度不一定正好填满弹性容器的宽度,可能会有留白。
多出来的留白(或剩余宽度)会按照 flex-grow(增长因子)的值分配给每个弹性子元素,flex-grow 的值为非负整数。如果一个弹性子元素的 flex-grow 值为 0,那么它的宽度不会超过 flex-basis 的值;如果某个弹性子元素的增长因子非 0,那么这些元素会增长到所有的剩余空间被分配完,也就意味着弹性子元素会填满容器的宽度。
flex-grow 的值越大,元素的“权重”越高,也就会占据更大的剩余宽度。一个 flex-grow:2 的子元素增长的宽度为 flex-grow: 1 的子元素的两倍
使用flex-shrink属性
flex-shrink 属性与 flex-grow 遵循相似的原则。计算出弹性子元素的初始主尺寸后,它们的累加值可能会超出弹性容器的可用宽度。如果不用 flex-shrink,就会导致溢出。
每个子元素的 flex-shrink 值代表了它是否应该收缩以防止溢出。如果某个子元素为flex-shrink: 0,则不会收缩;如果值大于 0,则会收缩至不再溢出。按照 flex-shrink 值的比例,值越大的元素收缩得越多。
弹性方向
Flexbox 的另一个重要功能是能够切换主副轴方向,用弹性容器的 flex-direction 属性控制。
它的初始值( row)控制子元素按从左到右的方向排列;指定flex-direction: column 能控制弹性子元素沿垂直方向排列(从上到下)。
Flexbox 还支持 row-reverse 让元素从右到左排列, column-reverse 让元素从下到上排列(如图 5-14所示)。
当弹性方向为 column,因此主轴发生了旋转,现在变成了从上到下(副轴变成了从左到右)。也就是对于弹性子元素而言, flex-basis、 flex-grow 和 flex-shrink现在作用于元素的高度而不是宽度。
如果指定了 flex: 1,因此在必要的时候子元素的高度会扩展到填满容器。
水平弹性盒子的大部分概念同样适用于垂直的弹性盒子( column 或 column-reverse),但是有一点不同:在 CSS 中处理高度的方式与处理宽度的方式在本质上不一样。
弹性容器会占据 100%的可用宽度,而高度则由自身的内容来决定。即使改变主轴方向,也不会影响这一本质。
弹性容器的高度由弹性子元素决定,它们会正好填满容器。
在垂直的弹性盒子里,子元素的flex-grow 和 flex-shrink 不会起作用,除非有“外力”强行改变弹性容器的高度。
在本章的网页里,“外力”就是从外层弹性盒子计算出来的高度(本章中当前弹性容器是另一个弹性容器的弹性子元素,因为弹性子元素是默认自动创建等高列,所以高度会被默认计算,高度为外层弹性子元素的高度二非内部弹性子元素高度)。
理解弹性容器的属性
flex-wrap 属性
flex-wrap 属性允许弹性子元素换到新的一行或多行显示。它可以设置为 nowrap(初始值)、 wrap 或者 wrap-reverse。
启用换行后,子元素不再根据 flex-shrink 值收缩,任何超过弹性容器的子元素都会换行显示。
如果弹性方向是 column 或 column-reverse,那么 flex-wrap 会允许弹性子元素换到新的一列显示,不过这只在限制了容器高度的情况下才会发生,否则容器会扩展高度以包含全部弹性子元素。
flex-flow 属性
flex-flow 属性是 flex-direction 和 flex-wrap 的简写。
例如, flex-frow: columnwrap 指定弹性子元素按照从上到下的方式排列,必要时换到新的一列。
justify-content 属性
当子元素未填满容器时, justify-content 属性控制子元素沿主轴方向的间距。
它的值包括几个关键字: flex-start、 flex-end、 center、 space-between 以及 space-around。
默认值 flex-start 让子元素从主轴的开始位置顺序排列,比如主轴方向为从左到右的话,开始位置就是左边。如果不设置外边距,那么子元素之间不会产生间距。
如果值为 flex-end,子元素就从主轴的结束位置开始排列, center 的话则让子元素居中。
值 space-between 将第一个弹性子元素放在主轴开始的地方,最后一个子元素放在主轴结束的地方,剩下的子元素间隔均匀地放在这两者之间的区域。
值 space-around 类似,只不过给第一个子元素的前面和最后一个子元素的后面也加上了相同的间距。
注意:间距是在元素的外边距之后进行计算的,而且 flex-grow 的值要考虑进来。也就是说,如果任意子元素的 flex-grow 的值不为 0,或者任意子元素在主轴方向的外边距值为 auto,justify-content 就失效了。
align-items 属性
justify-content 控制子元素在主轴方向的对齐方式, align-items 则控制子元素在副轴方向的对齐方式。
align-items 的初始值为 stretch,在水平排列的情况下让所有子元素填充容器的高度,在垂直排列的情况下让子元素填充容器的宽度,因此它能实现等高列。
其他的值让弹性子元素可以保留自身的大小,而不是填充容器的大小。(类似的概念有vertical-align 属性。)
- flex-start 和 flex-end 让子元素与副轴的开始或结束位置对齐。(如果是水平布局的话,则与容器的顶部或者底部分别对齐。)
- center 让元素居中。
- baseline 让元素根据每个弹性子元素的第一行文字的基线对齐。
align-content 属性
如果开启了换行(用 flex-wrap), align-content 属性就可以控制弹性容器内沿副轴方向每行之间的间距。
它支持的值有 flex-start、 flex-end、 center、 stretch(初始值)、space-between 以及 space-around。这些值对间距的处理类似上面的 justify-content。
理解弹性子元素的属性
align-self 属性
该属性控制弹性子元素沿着容器副轴方向的对齐方式。
它跟弹性容器的 align-items 属性效果相同,但是它能单独给弹性子元素设定不同的对齐方式。
auto 为初始值,会以容器的 alignitems 值为准。
其他值会覆盖容器的设置。 align-self 属性支持的关键字与 align-items 一样: flex-start、 flex-end、 center、 stretch 以及 baseline。
order 属性
正常情况下,弹性子元素按照在 HTML 源码中出现的顺序排列。
它们沿着主轴方向,从主轴的起点开始排列。
使用 order 属性能改变子元素排列的顺序。还可以将其指定为任意正负整数。如果多个弹性子元素有一样的值,它们就会按照源码顺序出现。
初始状态下,所有的弹性子元素的 order 都为 0。
指定一个元素的值为−1,它会移动到列表的最前面;指定为 1,则会移动到最后。可以按照需要给每个子元素指定 order 以便重新编排它们。这些值不一定要连续。
值得注意的地方
Flexbox 的一个有趣之处在于如何基于弹性子元素的数量和其中的内容量(及大小)来计算容器的大小。
因为如果网页很大,或者加载很慢时可能会产生奇怪的行为。
当浏览器加载内容时,它渐进渲染到了屏幕,即使此时网页的剩余内容还在加载。
假设有一个使用弹性盒子( flex-direction: row)实现的三列布局。如果其中两列的内容加载了,浏览器可能会在加载完第三列之前就渲染这两列。然后等到剩余内容加载完,浏览器会重新计算每个弹性子元素的大小,重新渲染网页。用户会短暂地看到两列布局,然后列的大小改变(可能改变特别大),并出现第三列。
只有一行多列的布局才会产生这个问题。如果主页面布局采用的是一列多行( flex-direction: column),就不会出现以上问题。
出现以上问题,建议是对整页布局的时候使用网格布局。
网格布局
CSS 网格可以定义由行和列组成的二维布局,然后将元素放置到网格中。有些元素可能只占据网格的一个单元,另一些元素则可能占据多行或多列。网格的大小既可以精确定义,也可以根据自身内容自动计算。你既可以将元素精确地放置到网格某个位置,也可以让其在网格内自动定位,填充划分好的区域。
跟 Flexbox 类似,网格布局也是作用于两级的 DOM 结构。
设置为 display: grid 的元素成为一个网格容器( grid container)。
它的子元素则变成网格元素( grid items)。
网格剖析
前面已经提及网格容器和网格元素,这些是网格布局的基本元素。另外四个重要的概念如图 6-3 所示。
- 网格线( grid line) ——网格线构成了网格的框架。一条网格线可以水平或垂直,并且位于一行或一列的任意一侧。如果指定了 grid-gap 的话,它就位于网格线上。
- 网格轨道( grid track) ——一个网格轨道是两条相邻网格线之间的空间。网格有水平轨道(行)和垂直轨道(列)。
- 网格单元( grid cell) ——网格上的单个空间,水平和垂直的网格轨道交叉重叠的部分。
- 网格区域( grid area) ——网格上的矩形区域,由一个到多个网格单元组成。该区域位于两条垂直网格线和两条水平网格线之间。
网格线的编号
网格轨道定义好之后,要将每个网格元素放到特定的位置上。浏览器给网格里的每个网格线都赋予了编号,如图 6-7 所示。 CSS 用这些编号指出每个元素应该摆放的位置。
可以在 grid-column 和 grid-row 属性中用网格线的编号指定网格元素的位置。
如果想要一个网格元素在垂直方向上跨越 1 号网格线到 3 号网格线,就需要给元素设置 grid-column: 1 / 3。或者设置 grid-row: 3 / 5 让元素在水平方向上跨越 3 号网格线到 5 号网格线。
这两个属性一起就能指定一个元素应该放置的网格区域。
grid-column 是 grid-column-start 和grid-column-end 的简写;
grid-row 是 grid-row-start 和 grid-row-end 的简写。
中间的斜线只在简写属性里用于区分两个值,斜线前后的空格不作要求。
关键字 span 来指定 grid-row 和 grid-column 的值。这个关键字告诉浏览器元素需要占据一个网格轨道。
如果没有指出具体是哪一行,所以会根据网格元素的布局算法( placement algorithm) 自动将其放到合适的位置。
布局算法会将元素放在网格上可以容纳该元素的第一处可用空间。
与Flexbox配合
学了网格之后,开发人员经常会问到 Flexbox,特别是会问这两种布局方式是否互斥。当然不会,它们是互补的。二者几乎是一起开发出来的,虽然它们的功能有一些重叠的地方,但是它们各自擅长的场景不一样。
在一个设计场景里,要根据特定的需求来做出选择。这两种布局方式有以下两个重要区别。
- Flexbox 本质上是一维的,而网格是二维的。
- Flexbox 是以内容为切入点由内向外工作的,而网格是以布局为切入点从外向内工作的。
因为 Flexbox 是一维的,所以它很适合用在相似的元素组成的行(或列)上。
它支持用flex-wrap 换行,但是没法让上一行元素跟下一行元素对齐。
相反,网格是二维的,旨在解决一个轨道的元素跟另一个轨道的元素对齐的问题。它们的区别如图 6-8 所示。
Flexbox 让你在一行或一列中安排一系列元素,但是它们的大小不需要明确指定,每个元素占据的大小根据自身的内容决定。
而在网格中,首先要描述布局,然后将元素放在布局结构中去。虽然每个网格元素的内容都能影响其网格轨道的大小,但是这同时也会影响整个轨道的大小,进而影响这个轨道里的其他网格元素的大小。
当设计要求元素在两个维度上都对齐时,使用网格。
当只关心一维的元素排列时,使用Flexbox。
在实践中,这通常(并非总是)意味着网格更适合用于整体的网页布局,而 Flexbox 更适合对网格区域内的特定元素布局。
继续用网格和 Flexbox,你就会对不同情况下该用哪种布局方式得心应手。
替代语法
布局网格元素还有另外两个替代语法:命名的网格线和命名的网格区域。
至于选择哪个纯属个人偏好。在某些设计中,一种语法会比另一种语法更好理解。下面分别介绍这两个语法。
命名的网格线
有时候记录所有网格线的编号实在太麻烦了,尤其是在处理很多网格轨道时。为了能简单点,可以给网格线命名,并在布局时使用网格线的名称而不是编号。声明网格轨道时,可以在中括号内写上网格线的名称,如下代码片段所示。
grid-template-columns: [start] 2fr [center] 1fr [end];
这条声明定义了两列的网格,三条垂直的网格线分别叫作 start、 center 和 end。
之后定义网格元素在网格中的位置时,可以不用编号而是用这些名称来声明,如下代码所示。
grid-column: start / center;
这条声明将网格元素放在 1 号网格线( start)到 2 号网格线( center)之间的区域。还可以给同一个网格线提供多个名称,比如下面的声明(为了可读性,这里将代码换行了)。
grid-template-columns: [left-start] 2fr
[left-end right-start] 1fr
[right-end];
在这条声明里, 2 号网格线既叫作 left-end 也叫作 right-start,之后可以任选一个名称使用。这里还有一个彩蛋:将网格线命名为 left-start 和 left-end,就定义了一个叫作 left 的区域,这个区域覆盖两个网格线之间的区域。 -start 和-end 后缀作为关键字,定义了两者之间的区域。
如果给元素设置 grid-column: left,它就会跨越从 left-start 到 left-end 的区域。
.container {
display: grid;
grid-template-columns: [left-start] 2fr // 给每个垂直的网格线命名
[left-end right-start] 1fr
[right-end];
grid-template-rows: repeat(4, [row] auto); // 将水平网格线命名为“row”
grid-gap: 1.5em;
max-width: 1080px;
margin: 0 auto;
}
header,
nav {
grid-column: left-start / right-end;
grid-row: span 1;
}
.main {
grid-column: left; // 跨越 left-start 到 left-end之间的区域
grid-row: row 3 / span 2; // 从第三行网格线开始放置元素,跨越两个网格轨道
}
.sidebar-top {
grid-column: right; // 跨越 right-start 到right-end 的区域
grid-row: 3 / 4;
}
.sidebar-bottom {
grid-column: right; // 跨越 right-start 到right-end 的区域
grid-row: 4 / 5;
}
可以以各种方式命名的网格线。它们在网格里的用法也是五花八门,这取决于每个网格特定的结构,比如可以实现如图 6-10 所示的布局。
这个场景展示了一种重复模式:每两个网格列为一组,在每组的两个网格轨道之前命名一条网格线( grid-template-columns: repeat(3, [col] 1fr 1fr))。然后就可以借助命名的网格线将一个元素定位到第二组网格列上( grid-column: col 2 / span 2)。
命名网格区域
不用计算或者命名网格线,直接用命名的网格区域将元素定位到网格中。
实现这一方法需要借助网格容器的 grid-template-areas 属性和网格元素的grid-area 属性。
.container {
display: grid;
grid-template-areas: "title title" // 将每个网格单元分配到一个命名的网格区域中
"nav nav"
"main aside1"
"main aside2";
grid-template-columns: 2fr 1fr; // 跟之前一样定义网格轨道的大小
grid-template-rows: repeat(4, auto);
grid-gap: 1.5em;
max-width: 1080px;
margin: 0 auto;
}
header {
grid-area: title; // 将每个网格元素放到一个命名的网格区域
}
nav {
grid-area: nav; // 将每个网格元素放到一个命名的网格区域
}
.main {
grid-area: main; // 将每个网格元素放到一个命名的网格区域
}
.sidebar-top {
grid-area: aside1; // 将每个网格元素放到一个命名的网格区域
}
.sidebar-bottom {
grid-area: aside2; // 将每个网格元素放到一个命名的网格区域
}
grid-template-areas 属性使用了一种 ASCII art 的语法,可以直接在 CSS 中画一个可视化的网格形象。该声明给出了一系列加引号字符串,每一个字符串代表网格的一行,字符串内用空格区分每一列。
注意:每个命名的网格区域必须组成一个矩形。不能创造更复杂的形状,比如 L或者 U型。
还可以用句点( .)作为名称,这样便能空出一个网格单元。比如,以下代码定义了四个网格区域,中间围绕着一个空的网格单元。
grid-template-areas: "top top right"
"left . right"
"left bottom bottom";
当你构建一个网格时,选择一种舒适的语法即可。
网格布局共设计了三种语法:编号的网格线、命名的网格线、命名的网格区域。
最后一个可能更受广大开发人员喜爱,尤其是明确知道每个网格元素的位置时,这种方式用起来更舒服。
显式和隐式网格
当元素是从数据库获取时,元素的个数可能是未知的。在这些情况下,以一种宽松的方式定义网格可能更合理,剩下的交给布局算法来放置网格元素。
这时需要用到隐式网格( implicit grid)。使用 grid-template-* 属性定义网格轨道时,创建的是显式网格( explicit grid),但是有些网格元素仍然可以放在显式轨道外面,此时会自动创建隐式轨道以扩展网格,从而包含这些元素。
图 6-11 里的网格只在每个方向上指定了一个网格轨道。当把网格元素放在第二个轨道( 2 号和 3 号网格线之间)时,就会自动创建轨道来包含该元素。
隐式网格轨道默认大小为 auto,也就是它们会扩展到能容纳网格元素内容。可以给网格容器设置 grid-auto-columns 和 grid-auto-rows,为隐式网格轨道指定一个大小(比如, grid-auto-columns: 1fr)。
接下来用隐式网格实现另一个网页布局。
这个网页是一个照片墙,如图 6-12 所示。在这个布局中,将设置列的网格轨道,但是网格行是隐式创建的。这样网页不必关心照片的数量,它能适应任意数量的网格元素。只要照片需要换行显示,就会隐式创建新的一行。
这个布局很有意思,因为它用 Flexbox 或者浮动很难实现。这个例子充分展示了网格特有的能力。
<div class="portfolio">
<figure class="featured"> // 加上了 featured 类,之后会让这些元素比其他图片大。
<img src="images/monkey.jpg" alt="monkey" />
<figcaption>Monkey</figcaption>
</figure>
<figure>
<img src="images/eagle.jpg" alt="eagle" />
<figcaption>Eagle</figcaption>
</figure>
<figure class="featured">
<img src="images/bird.jpg" alt="bird" />
<figcaption>Bird</figcaption>
</figure>
<figure>
<img src="images/bear.jpg" alt="bear" />
<figcaption>Bear</figcaption>
</figure>
<figure class="featured">
<img src="images/swan.jpg" alt="swan" />
<figcaption>Swan</figcaption>
</figure>
<figure>
<img src="images/elephants.jpg" alt="elephants" />
<figcaption>Elephants</figcaption>
</figure>
<figure>
<img src="images/owl.jpg" alt="owl" />
<figcaption>Owl</figcaption>
</figure>
</div>
使用 grid-auto-rows 为所有的隐式网格行指定一 个 1fr 的大小,每一行拥有相同的高度。该布局还引入了两个新概念: auto-fill 和 minmax()函数。
body {
background-color: #709b90;
font-family: Helvetica, Arial, sans-serif;
}
.portfolio {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); // 将最小列宽设置为 200px,自动填充网格
grid-auto-rows: 1fr; // 将隐式水平网格轨道的大小设置为 1fr
grid-gap: 1em;
}
.portfolio > figure {
margin: 0;
}
.portfolio img {
max-width: 100%;
}
.portfolio figcaption {
padding: 0.3em 0.8em;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
text-align: right;
}
有时候我们不想给一个网格轨道设置固定尺寸,但是又希望限制它的最小值和最大值。
这时候需要用到 minmax()函数。它指定两个值:最小尺寸和最大尺寸。浏览器会确保网格轨道的大小介于这两者之间。(如果最大尺寸小于最小尺寸,最大尺寸就会被忽略。)通过指定minmax(200px, 1fr),浏览器确保了所有的轨道至少宽 200px。
repeat()函数里的 auto-fill 关键字是一个特殊值。设置了之后,只要网格放得下,浏览器就会尽可能多地生成轨道,并且不会跟指定大小( minmax()值)的限制产生冲突。
auto-fill 和 minmax(200px, 1fr)加在一起,就会让网格在可用的空间内尽可能多地产生网格列,并且每个列的宽度不会小于 200px。因为所有轨道的大小上限都为 1fr(最大值),所以所有的网格轨道都等宽。
如果网格元素不够填满所有网格轨道, auto-fill 就会导致一些空的网格轨道。(如果有剩余空间的话,
auto-fill
会创建额外的空列。而auto-fit
则以最后一个 Gird 项目结束 Gird 容器,不管是否还有额外的空间存在。)如果不希望出现空的网格轨道,可以使用 auto-fit 关键字代替 auto-fill。它会让非空的网格轨道扩展,填满可用空间。
[译]CSS Grid #13:auto-fill 和 auto-fit 关键字
具体选择 auto-fill 还是 auto-fit 取决于你是想要确保网格轨道的大小,还是希望整个网格容器都被填满。我一般倾向于 auto-fit。
添加变化
我们让特定图片(本例里的鸟、天鹅等)变大,增加一些视觉上的趣味性。现在每个网格元素都占据了 1×1 的区域,将特定图片的尺寸增加到 2×2 的网格区域。可以用 featured类选择这些元素,让它们在水平和垂直方向上都占据两个网格轨道。
问题来了,由于元素按顺序排列,增加某些网格元素的大小会导致网格中出现空白区域,如图 6-14 所示。鸟所在的是第三个网格元素,但是因为它的尺寸较大,所以第二张老鹰图的右侧空间容纳不下它。因此,它下降到了下一个网格轨道。
当不指定网格上元素的位置时,元素会按照其布局算法自动放置。默认情况下,布局算法会按元素在标记中的顺序将其逐列逐行摆放。当一个元素无法在某一行容纳(也就是说该元素占据了太多网格轨道)时,算法会将它移动到下一行,寻找足够大的空间容纳它。
网格布局模块规范提供了另一个属性 grid-auto-flow,它可以控制布局算法的行为。它的初始值是 row,上一段描述的就是这个值的行为。如果值为 column,它就会将元素优先放在网格列中,只有当一列填满了,才会移动到下一行。
还可以额外加一个关键字 dense(比如, grid-auto-flow: column dense)。它让算法紧凑地填满网格里的空白,尽管这会改变某些网格元素的顺序。加上这个关键字,小元素就会“回填”大元素造成的空白区域。效果如图 6-15 所示。
上紧凑的 auto-flow 选项 dense,小网格元素会填满大的元素造成的空白区域。源码顺序仍然是猴子、老鹰、鸟、熊,但是熊被挪到了鸟之前,填满了空白。
将以下 添加到样式表。它放大了特定图片,让其在水平和垂直方向上均占据两个网格轨道,并且使用了紧凑的 auto-flow。
.portfolio {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-auto-rows: 1fr;
grid-gap: 1em;
grid-auto-flow: dense; // 开启紧凑的网格布局算法
}
.portfolio .featured {
grid-row: span 2; // 将特定图片放大,在水平和垂直方向上各占据两个网格轨道
grid-column: span 2; // 将特定图片放大,在水平和垂直方向上各占据两个网格轨道
}
这段代码使用了 grid-auto-flow: dense,等价于 grid-auto-flow: row dense。(前面的写法里隐含了 row,因为初始值就是 row。)然后选择特定的图片,将其设置为在水平和垂直方向上各占据两个网格轨道。注意,本例只用了 span 关键字,没有明确地将任何一个网格元素放到某个网格轨道上。这样布局算法就会将网格元素放到它觉得合适的地方。
需要注意的是,紧凑的 auto-flow 方式会导致元素出现的顺序跟 HTML 里不一致。当使用键盘( Tab 键)或者使用以源码顺序而非以显示顺序为准的屏幕阅读器来浏览网页时,用户可能会感到困惑。
提示:子网格
网格有一个限制是要求用特定的 DOM 结构,也就是说,所有的网格元素必须是网格容器的直接子节点。因此,不能将深层嵌套的元素在网格上对齐。
可以给网格元素加上 display: grid,在外层网格里创建一个内部网格,但是内部网格的元素不一定会跟外层网格的轨道对齐。一个网格里的子元素的大小也不能影响到另一个网格的网格轨道大小。
将来可以使用子网格( subgrid)来解决这个问题。通过给一个网格元素设置 display: subgrid,将其变成自己的内部网格容器,网格轨道跟外部网格的轨道对齐。不幸的是,这个特性还没有被任何浏览器实现,因此它被推迟到网格规范的 Level2 版本中。
让网格元素填满网格轨道
较大的图片没有完全填满网格单元,在图片下面留了一片小小的空白。理想情况下,每个元素的顶部和底部都应该跟同一网格轨道上的元素对齐。现在顶部对齐了,底部却没有像图 6-16 那样对齐。
默认情况下,每个网格元素都会扩展并填满整个网格区域,但是子元素不会,因此网格区域出现了多余的高度。
一个简单的解决办法是用 Flexbox。设置每个网格元素为弹性容器,方向为 column,元素会从上到下垂直排列。然后给图片标签加上 flex-grow,强制拉伸图片填充空白区域。
但是拉伸图片并不可取,因为这会改变图片的宽高比,导致图片变形。好在 CSS 为控制这一行为提供了一个特殊属性 object-fit。默认情况下,一个<img>的 object-fit 属性值为fill(会改变宽高比),也就是说整个图片会缩放,以填满<img>元素。你也可以设置其他值改变默认行为。
比如, object-fit 属性的值还可以是 cover 和 contain(如图 6-17 所示)。这些值告诉浏览器,在渲染盒子里改变图片的大小,但是不要让图片变形。
- cover:扩展图片,让它填满盒子(导致图片一部分被裁剪)。
- contain:缩放图片,让它完整地填充盒子(导致盒子里出现空白)。
这里有两个概念要区分清楚:盒子(由<img>元素的宽和高决定)和渲染的图片。默认情况下,这二者大小相等。 object-fit 属性让我们能在盒子内部控制渲染图片的大小,同时保持盒子的大小不变。
因为用 flex-grow 属性拉伸了图片,所以应该给它加上 object-fit: cover 防止渲染的图片变形。作为妥协,图片的边缘会被裁掉一部分。
特性查询
现在你已经大体上掌握了网格布局,你可能会问:难道要等到所有的浏览器都支持网格了才能使用它吗?不。只要你想用,现在就可以,但是要考虑如果浏览器不支持网格时应该如何布局,并给出回退的样式。
CSS 最近添加了一个叫作特性查询( feature query)的功能,该功能有助于解决这个问题,如下代码片段所示。
@supports (display: grid) {
...
}
@supports 规则后面跟着一个小括号包围的声明。如果浏览器理解这个声明(在本例中,浏览器支持网格),它就会使用大括号里面的所有样式规则。如果它不理解小括号里的声明,就不会使用这些样式规则。
@supports 规则可以用来查询所有的 CSS 特性。比如,用@supports (display: flex)来查询是否支持 Flexbox,用@supports (mix-blend-mode: overlay) 来查询是否支持混合模式
特性查询还有以下几种写法。
- @supports not(<declaration>)——只有当不支持查询声明里的特性时才使用里面的样式规则。
- @supports (<declaration>) or (<declaration>)——查询声明里的两个特性只要有一个支持就使用里面的样式规则。
- @supports (<declaration>) and (<declaration>)——查询声明里的两个特性都支持才使用里面的样式规则。
这些写法还可以结合起来查询更复杂的情况。关键字 or 适合查询带浏览器前缀的属性(如下声明所示)。
@supports (display: grid) or (display: -ms-grid)
这句声明既指定了支持非前缀版本属性的浏览器,也指定了要求用-ms-前缀的旧版 Edge 浏览器。需要注意的是,旧版 Edge 对网格的部分支持不如现代浏览器稳健。在旧版 Edge 中用特性查询来支持网格布局,可能麻烦比收益更大。因此最好是忽略它,让旧版 Edge 渲染为回退布局。
对齐
网格布局模块规范里的对齐属性有一些跟 Flexbox 相同,还有一些是新属性。FlexBox 已经介绍了其中的大部分,现在我们看看如何在网格里面使用这些属性。如果我们想要对网格布局有更多的控制,这些属性可能会很方便。
CSS 给网格布局提供了三个调整属性 : justify-content 、 justify-items 、justify-self。这些属性控制了网格元素在水平方向上的位置。
还有三个对齐属性: align-content、 align-items、 align-self。这些属性控制网格元素在垂直方向上的位置。
可以用 justify-content 和 align-content 设置网格容器内的网格轨道在水平方向和垂直方向上的位置,特别是当网格元素的大小无法填满网格容器时。参考以下代码。
.grid {
display: grid;
height: 1200px;
grid-template-rows: repeat(4, 200px);
}
它明确指定了网格容器的高度为 1200px,但是只定义了高 800px 的有效水平网格轨道。align-content 属性指定了网格轨道如何在剩下的 400px 空间内分布。它可以设为以下值。
- start——将网格轨道放到网格容器的上/左( Flexbox 里则是 flex-start)。
- end——将网格轨道放在网格容器的下/右( Flexbox 里则是 flex-end)。
- center——将网格轨道放在网格容器的中间。
- stretch——将网格轨道拉伸至填满网格容器。
- space-between——将剩余空间平均分配到每个网格轨道之间(它能覆盖任何 grid-gap值)。
- space-around——将空间分配到每个网格轨道之间,且在两端各加上一半的间距。
- space-evenly——将空间分配到每个网格轨道之间,且在两端各加上同等大小的间距( Flexbox 规范不支持)。
定位和层叠上下文
position 属性的初始值是 static。前面的章节里用的都是这个静态定位。如果把它改成其他值,我们就说元素就被定位了。而如果元素使用了静态定位,那么就说它未被定位。
固定定位
给一个元素设置 position: fixed 就能将元素放在视口的任意位置。这需要搭配四种属性一起使用: top、 right、 bottom 和 left。
这些属性的值决定了固定定位的元素与浏览器视口边缘的距离。
设置这四个值还隐式地定义了元素的宽高。
比如指定 left: 2em; right: 2em 表示元素的左边缘距离视口左边 2em,右边缘距离视口右边 2em。因此元素的宽度等于视口总宽度减去4em。 top、 bottom 和视口高度也是这样的关系。
固定元素从文档流中移除了,所以它不再影响页面其他元素的位置。
别的元素会跟随正常文档流,就像固定元素不存在一样。
也就是说它们通常会在固定元素下面排列,视觉上被遮挡。
绝对定位
固定定位让元素相对视口定位,此时视口被称作元素的包含块( containing block)。
绝对定位的行为也是如此,只是它的包含块不一样。
绝对定位不是相对视口,而是相对最近的祖先定位元素。
跟固定元素一样,属性 top、 right、 bottom 和 left 决定了元素的边缘在包含块里的位置。
通常情况下,包含块是元素的父元素。如果父元素未被定位,那么浏览器会沿着 DOM 树往上找它的祖父、曾祖父,直到找到一个定位元素,用它作为包含块。
相对定位
相对定位可能是最不被理解的定位类型。当第一次给元素加上 position: relative 的时候,你通常看不到页面上有任何视觉改变。相对定位的元素以及它周围的所有元素,都还保持着原来的位置。
如果加上 top、 right、 bottom 和 left 属性,元素就会从原来的位置移走,但是不会改变它周围任何元素的位置。
如图 7-4 所示,四个 inline-block 元素,给第二个元素加上三个额外的属性: position: relative、 top: 1em、 left: 2em,将其从初始位置移走,但是其他元素没有受到影响。它们还是围绕着被移走元素的初始位置,跟随着正常的文档流。
设置 top: 1em 将元素从原来的顶部边缘向下移动了 1em;设置 left: 2em 将元素从它来的左侧边缘向右移动了 2em。这可能导致元素跟它下面或者旁边的元素重叠。在定位中,也可以使用负值,比如 bottom: -1em 也可以像 top: 1em 那样将元素向下移动 1em。
说明:跟固定或者绝对定位不一样, 不能用 top、 right、 bottom 和 left 改变相对定位元素的大小。这些值只能让元素在上、下、左、右方向移动。可以用 top 或者 bottom,但它们不能一起用( bottom 会被忽略)。同理,可以用 left 或 right,但它们也不能一起用( right 会被忽略)。
有时可以用这些属性调整相对元素的位置,把它挤到某个位置,但这只是相对定位的一个冷门用法。
更常见的用法是使用 position: relative 给它里面的绝对定位元素创建一个包含块。
层叠上下文和 z-index
理解渲染过程和层叠顺序
浏览器将 HTML 解析为 DOM 的同时还创建了另一个树形结构,叫作渲染树( render tree)。
它代表了每个元素的视觉样式和位置。同时还决定浏览器绘制元素的顺序。顺序很重要,因为如果元素刚好重叠,后绘制的元素就会出现在先绘制的元素前面。
通常情况下(使用定位之前),元素在 HTML 里出现的顺序决定了绘制的顺序。
<div>one</div>
<div>two</div>
<div>three</div>
它们的层叠行为如图 7-11 所示。这里使用了负的外边距让元素重叠,但并未使用任何定位。后出现在标记里的元素会绘制在先出现的元素前面。
定位元素时,这种行为会改变。浏览器会先绘制所有非定位的元素,然后绘制定位元素。
默认情况下,所有的定位元素会出现在非定位元素前面。
如图 7-12 所示,给前两个元素加了position: relative,它们就绘制到了前面,覆盖了静态定位的第三个元素,尽管元素在 HTML里的顺序并未改变。
注意,在定位元素里,第二个定位元素还是出现在第一个定位元素前面。定位元素会被放到前面,但是基于源码的层叠关系并没有改变。
可以改变定位元素在源码的位置可以修改渲染顺序,解决网页层叠问题。
改变固定定位元素的标记位置通常不会产生不好的影响,但是对相对定位或绝对定位的元素来说,通常无法用改变标记位置的方法解决层叠问题。
相对定位依赖于文档流,绝对定位元素依赖于它的定位祖先节点。
这时候需要用 z-index 属性来控制它们的层叠行为。
用z-index控制层叠顺序
z-index 属性的值可以是任意整数(正负都行)。
z 表示的是笛卡儿 x-y-z 坐标系里的深度方向。拥有较高 z-index 的元素出现在拥有较低 z-index 的元素前面。
拥有负数 z-index 的元素出现在静态元素后面。
使用 z-index 是解决网页层叠问题的第二个方法。该方法不要求修改 HTML 的结构。
z-index 的行为很好理解,但是使用它时要注意两个小陷阱。
第一, z-index 只在定位元素上生效,不能用它控制静态元素。
第二,给一个定位元素加上 z-index 可以创建层叠上下文。
理解层叠上下文
一个层叠上下文包含一个元素或者由浏览器一起绘制的一组元素。
其中一个元素会作为层叠上下文的根,比如给一个定位元素加上 z-index 的时候,它就变成了一个新的层叠上下文的根。
所有后代元素就是这个层叠上下文的一部分。
不要将层叠上下文 跟 BFC 弄混了,它们是两个独立的概念,尽管不一定互斥。层叠上下文负责决定哪些元素出现在另一些元素前面,而 BFC 负责处理文档流,以及元素是否会重叠。
层叠上下文之外的元素无法叠放在层叠上下文内的两个元素之间。
换句话说,如果一个元素叠放在一个层叠上下文前面,那么层叠上下文里没有元素可以被拉到该元素前面。
同理,如果一个元素被放在层叠上下文后面,层叠上下文里没有元素能出现在该元素后面。
总结:z-index 只控制元素在它所处层叠上下文内的层叠顺序,不会影响到其他层叠上下文。
说明:给一个定位元素加上 z-index 是创建层叠上下文最主要的方式,但还有别的属性也能创建,比如小于 1 的 opacity 属性, 还有 transform、 filter 属性。
由于这些属性主要会影响元素及其子元素渲染的方式,因此一起绘制父子元素。文档根节点( <html>)也会给整个页面创建一个顶级的层叠上下文。
所有层叠上下文内的元素会按照以下顺序,从后到前叠放:
- 层叠上下文的根
- z-index 为负的定位元素(及其子元素)
- 非定位元素
- z-index 为 auto 的定位元素(及其子元素)
- z-index 为正的定位元素(及其子元素)
如果发现 z-index 没有按照预期表现,就在 DOM 树里往上找到元素的祖先节点,直到发现层叠上下文的根。然后给它设置 z-idnex,将整个层叠上下文向前或者向后放。还要注意多个层叠上下文嵌套的情况。
有些开发人员会忍不住给页面的大量元素使用定位。一定要克制这种冲动。定位用得越多,网页就越复杂,也就越难调试。如果你定位了大量元素,就回头评估一下现在的情况,尤其是当你发现很难调试出自己想要的布局时,一定要反思。如果可以用别的方法实现某个布局,应该优先用那些方法。
如果能够依靠文档流,而不是靠明确指定定位的方式实现布局,那么浏览器会帮我们处理好很多边缘情况。记住,定位会将元素拉出文档流。一般来说,只有在需要将元素叠放到别的元素之前时,才应该用定位。
粘性定位
人们已经用四种主要的定位类型(静态、固定、绝对以及相对)很长时间了,不过现在浏览器还提供了一种新的定位类型: 粘性定位( sticky positioning)。
它是相对定位和固定定位的结合体:正常情况下,元素会随着页面滚动,当到达屏幕的特定位置时,如果用户继续滚动,它就会“锁定”在这个位置。最常见的用例是侧边栏导航。
<!doctype html>
<head>
<style>
body {
font-family: Helvetica, Arial, sans-serif;
min-height: 200vh;
margin: 0;
}
button {
padding: .5em .7em;
border: 1px solid #8d8d8d;
background-color: white;
font-size: 1em;
}
.top-banner {
padding: 1em 0;
background-color: #ffd698;
}
.top-banner-inner {
width: 80%;
max-width: 1000px;
margin: 0 auto;
}
.modal {
display: none;
}
.modal-backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1;
}
.modal-body {
position: fixed;
top: 3em;
bottom: 3em;
right: 20%;
left: 20%;
padding: 2em 3em;
background-color: white;
overflow: auto;
z-index: 2;
}
.modal-close {
position: absolute;
top: 0;
right: 0;
padding: 0.3em;
font-size: 2em;
height: 1.5em;
width: 1.5em;
cursor: pointer;
border: 0;
}
.modal-close::before {
display: block;
content: '\00D7';
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
}
.container {
display: flex;
width: 80%;
max-width: 1000px;
margin: 1em auto;
min-height: 100vh; // 设置足够高度,方便粘性定位元素滚动
}
.dropdown {
display: inline-block;
position: relative;
}
.dropdown-label {
padding: 0.5em 2em 0.5em 1.5em;
border: 1px solid #ccc;
background-color: #eee;
}
.dropdown-label::after {
content: "";
position: absolute;
right: 1em;
top: 1em;
border: 0.3em solid;
border-color: black transparent transparent;
}
.dropdown:hover .dropdown-label::after {
top: 0.7em;
border-color: transparent transparent black;
}
.dropdown-menu {
display: none;
position: absolute;
left: 0;
top: 2.1em;
min-width: 100%;
background-color: #eee;
}
.dropdown:hover .dropdown-menu {
display: block;
}
.submenu {
padding-left: 0;
margin: 0;
list-style-type: none;
border: 1px solid #999;
}
.submenu > li + li {
border-top: 1px solid #999;
}
.submenu > li > a {
display: block;
padding: .5em 1.5em;
background-color: #eee;
color: #369;
text-decoration: none;
}
.submenu > li > a:hover {
background-color: #fff;
}
.col-main {
flex: 1 80%;
}
.col-sidebar {
flex: 20%;
}
.affix {
position: sticky; // 给侧边栏的菜单添加粘滞定位。它会停在距离视口顶部 1em 的位置
top: 1em;
}
</style>
</head>
<body>
<header class="top-banner">
<div class="top-banner-inner">
<p>Find out what's going on at Wombat Coffee each
month. Sign up for our newsletter:
<button id="open">Sign up</button>
</p>
</div>
</header>
<div class="modal" id="modal">
<div class="modal-backdrop"></div>
<div class="modal-body">
<button class="modal-close" id="close">
<span class="sr-only">close</span>
</button>
<h2>Wombat Newsletter</h2>
<p>Sign up for our monthly newsletter. No spam.
We promise!</p>
<form>
<p>
<label for="email">Email address:</label>
<input type="text" name="email"/>
</p>
<p><button type="submit">Submit</button></p>
</form>
</div>
</div>
<div class="container">
<main class="col-main">
<nav>
<div class="dropdown">
<div class="dropdown-label">Main Menu</div>
<div class="dropdown-menu">
<ul class="submenu">
<li><a href="/">Home</a></li>
<li><a href="/coffees">Coffees</a></li>
<li><a href="/brewers">Brewers</a></li>
<li><a href="/specials">Specials</a></li>
<li><a href="/about">About us</a></li>
</ul>
</div>
</div>
</nav>
<h1>Wombat Coffee Roasters</h1>
</main>
<aside class="col-sidebar">
<div class="affix">
<ul class="submenu">
<li><a href="/">Home</a></li>
<li><a href="/coffees">Coffees</a></li>
<li><a href="/brewers">Brewers</a></li>
<li><a href="/specials">Specials</a></li>
<li><a href="/about">About us</a></li>
</ul>
</div>
</aside>
</div>
<script type="text/javascript">
var button = document.getElementById('open');
var close = document.getElementById('close');
var modal = document.getElementById('modal');
button.addEventListener('click', function(event) {
event.preventDefault();
modal.style.display = 'block';
});
close.addEventListener('click', function(event) {
event.preventDefault();
modal.style.display = 'none';
});
</script>
</body>
声明 affix 元素粘性定位。 top 值设置了元素最终固定的位置:距离视口的顶部 1em。
因为粘性元素永远不会超出父元素的范围,所以本例中 affix 不会超出 col-sidebar 的范围。
当滚动页面的时候, col-sidebar 会一直正常滚动,但是 affix 会在滚动到特定位置时停下来。
如果继续滚动得足够远,粘性元素还会恢复滚动。这种情况只在父元素的底边到达粘性元素的底边时发生。
注意,只有当父元素的高度大于粘性元素时才会让粘性元素固定,因此这里我特意给弹性容器加上 min-height,以便让父元素足够高。
响应式设计
给所有用户提供同一份 HTML 和 CSS。
通过使用几个关键技术,根据用户浏览器视口的大小(或者屏幕分辨率)让内容有不一样的渲染结果。
只需要创建一个网站,就可以在智能手机、平板,或者其他任何设备上运行。
网页设计师Ethan Marcotte 称这种方式为响应式设计( responsive design)。
响应式设计的三大原则如下:
- 移动优先。这意味着在实现桌面布局之前先构建移动版的布局。
- @media 规则。使用这个样式规则,可以为不同大小的视口定制样式。用这一语法,通常叫作媒体查询( media queries),写的样式只在特定条件下才会生效。
- 流式布局。这种方式允许容器根据视口宽度缩放尺寸。
重点:做响应式设计时,一定要确保 HTML 包含了各种屏幕尺寸所需的全部内容。你可以对每个屏幕尺寸应用不同的 CSS,但是它们必须共享同一份 HTML。
移动优先
开发移动版网页有很多限制:屏幕空间受限、网络更慢。用户跟网页交互的方式也不一样:可以打字,但是用着很别扭,不能将鼠标移动到元素上触发效果等。
如果一开始就设计一个包含全部交互的网站,然后再根据移动设备的限制来制约网站的功能,那么一般会以失败告终。
而移动优先的方式则会让你设计网站的时候就一直想着这些限制。一旦移动版的体验做好了(或者设计好了),就可以用“渐进增强”( progressive enhancement)的方式为大屏用户增加体验。
移动端布局一般是很朴素的设计。
移动版设计主要关注的是内容,移动版设计就是内容的设计。
在页面设计时需要思考较大的视口该如何设计。虽然要先给移动端写布局,但是心里装着整体的设计,才能帮助我们在实现过程中做出合适的决定。
提示:当设计移动触屏设备的时候,确保所有的关键动作元素都足够大,能够用一个手指轻松点击。千万不要让用户放大页面,才能点中一个小小的按钮或者链接。
给视口添加meta标签
现在移动版设计已经完成,但是还差一个重要细节:视口的 meta 标签。
这个 HTML 标签告诉移动设备,你已经特意将网页适配了小屏设备。如果不加这个标签,移动浏览器会假定网页不是响应式的,并且会尝试模拟桌面浏览器,那之前的移动端设计就白做了。
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> //视口 meta 标签
<title>Wombat Coffee Roasters</title>
<link href="styles.css" />
</head>
meta 标签的 content 属性里包含两个选项。首先,它告诉浏览器当解析 CSS 时将设备的宽度作为假定宽度,而不是一个全屏的桌面浏览器的宽度。
其次当页面加载时,它使用initial-scale 将缩放比设置为 100%。
这些选项还可以设置为其他值,但是以上配置应该最能满足实际需求。
例如,你可以明确设置 width=320 让浏览器假定视口宽度为 320px,但是通常不建议这样,因为移动设备的尺寸范围很广。通过使用 device-width,可以用最合适的尺寸渲染内容。
此外 content 属性还有第三个选项 user-scalable=no,阻止用户在移动设备上用两个手指缩放。通常这个设置在实践中并不友好,不推荐使用。当链接太小不好点击,或者用户想要把某个图片看得更清楚时,这个设置会阻止他们缩放页面。
媒体查询
响应式设计的第二个原则是使用媒体查询。 媒体查询( media queries)允许某些样式只在页面满足特定条件时才生效。这样就可以根据屏幕大小定制样式。可以针对小屏设备定义一套样式,针对中等屏幕设备定义另一套样式,针对大屏设备再定义一套样式,这样就可以让页面的内容拥有多种布局。
媒体查询使用@media 规则选择满足特定条件的设备。一条简单的媒体查询如下代码所示。
@media (min-width: 560px) {
.title > h1 {
font-size: 2.25rem;
}
}
@media 规则会进行条件检查,只有满足所有的条件时,才会将这些样式应用到页面上。
媒体查询里面的规则仍然遵循常规的层叠顺序。
它们可以覆盖媒体查询外部的样式规则(根据选择器的优先级或者源码顺序),同理,也可能被其他样式覆盖。
媒体查询本身不会影响到它里面选择器的优先级。
警告: 在媒体查询断点中推荐使用 em 单位。
在各大主流浏览器中,当用户缩放页面或者改变默认的字号时,只有 em 单位表现一致。以 px 或者 rem 单位为断点在 Safari 浏览器里不太可靠。同时当用户默认字号改变的时候, em 还能相应地缩放,因此它更适合当断点。
在前面的例子里用的是 px,但是在媒体查询里更适合用 em, em 是基于浏览器默认字号的(通常是 16px)。下面将 560px 改成 35em(560 / 16)。
媒体查询的类型
还可以进一步将两个条件用 and 关键字联合起来组成一个媒体查询,如下代码所示。
@media (min-width: 20em) and (max-width: 35em) { ... }
这种联合媒体查询只在设备同时满足这两个条件时才生效。
如果设备只需要满足多个条件之一,可以用逗号分隔,如下代码所示。
@media (max-width: 20em), (min-width: 35em) { ... }
min-width、 max-width 等
min-width 匹配视口大于特定宽度的设备, max-width 匹配视口小于特定宽度的设备。它们被统称为媒体特征( media feature)。
min-width 和 max-width 是目前用得最广泛的媒体特征,但还有一些别的媒体特征,如下所示。
- (min-height: 20em)——匹配高度大于等于 20em 的视口。
- (max-height: 20em)——匹配高度小于等于 20em 的视口。
- (orientation: landscape)——匹配宽度大于高度的视口。
- (orientation: portrait)——匹配高度大于宽度的视口。
- (min-resolution: 2dppx)——匹配屏幕分辨率大于等于 2dppx( dppx 指每个 CSS 像素里包含的物理像素点数)的设备,比如视网膜屏幕。
- (max-resolution: 2dppx)——匹配屏幕分辨率小于等于 2dppx 的设备。
提示 媒体查询还可以放在<link>标签中。在网页里加入<link rel="stylesheet" media="(min-width: 45em)" href="large-screen.css" />,只有当 min-width媒体查询条件满足的时候才会将 large-screen.css 文件的样式应用到页面。
然而不管视口宽度如何,样式表都会被下载。这种方式只是为了更好地组织代码,并不会节省网络流量。
媒体类型
最后一个媒体查询的选项是媒体类型( media type)。常见的两种媒体类型是 screen 和print。
使用 print 媒体查询可以控制打印时的网页布局,这样就能在打印时去掉背景图(节省墨水),隐藏不必要的导航栏。
当用户打印网页时,他们通常只想打印主体内容。
说明:考虑打印样式
开发 CSS 的时候,通常在事后才会处理打印样式,而且只在需要的时候才会去考虑,但还是有必要思考用户是否想要打印网页的。为了帮助用户打印网页,需要采取一些通用步骤。大多数情况下,需要将基础打印样式放在@media print {...}媒体查询内。
使用 display: none 隐藏不重要的内容,比如导航菜单和页脚。当用户打印网页时,他们绝大多数情况下只关心网页的主体内容。
还可以将整体的字体颜色设置成黑色,去掉文字后面的背景图片和背景色。大多数情况下,用通用选择器就能实现。下面的代码使用了!important,这样就不必担心被后面的代码覆盖。
@media print {
* {
color: black !important;
background: none !important;
}
}
给网页添加断点
通常来说,移动优先的开发方式意味着最常用的媒体查询类型应该是 min-width。
在任何媒体查询之前,最先写的是移动端样式,然后设置越来越大的断点。
最优先的是移动端样式,因为它们不在媒体查询里,所以这些样式对所有断点都有效。然后是针对中等屏幕的媒体查询,其中的规则基于移动端样式构建并且会覆盖移动端样式。最后是针对大屏幕的媒体查询,在这里添加网页最后的布局。
有的设计可能只需要一个断点,有的设计可能需要多个断点。对网页上有很多元素来讲,无须给每个断点都添加样式,因为在小屏幕或者中等屏幕的断点下添加的样式规则在大屏幕的断点下也完全有效。
有时候移动端的样式可能很复杂,在较大的断点里面需要花费较大篇幅去覆盖样式。此时需要将这些样式放在 max-width 媒体查询中,这样就只对较小的断点生效,但是用太多的max-width 媒体查询也很有可能是没有遵循移动优先原则所致。
max-width 是用来排除某些规则的方式,而不是一个常规手段。
断点的选择
有时候会忍不住想要根据设备选择断点。这个 iPhone 7 宽多少像素,那个平板设备宽多少像素,等等。
不要总想着设备。市面上有成百上千中设备和屏幕分辨率,无法逐一测试。相反,应该选择适合设计的断点,这样不管在什么设备上,都能有很好的表现。
流式布局
响应式设计的第三个也是最后一个原则是流式布局( fluid layout)。
流式布局,有时被称作液体布局( liquid layout),指的是使用的容器随视口宽度而变化。
它跟固定布局相反,固定布局的列都是用 px 或者 em 单位定义。固定容器(比如,设定了 width: 800px 的元素)在小屏上会超出视口范围,导致需要水平滚动条,而流式容器会自动缩小以适应视口。
在流式布局中,主页面容器通常不会有明确宽度,也不会给百分比宽度,但可能会设置左右内边距,或者设置左右外边距为 auto,让其与视口边缘之间产生留白。
也就是说容器可能比视口略窄,但永远不会比视口宽。
在主容器中,任何列都用百分比来定义宽度(比如,主列宽 70%,侧边栏宽 30%)。这样无论屏幕宽度是多少都能放得下主容器。用 Flexbox 布局也可以,设置弹性元素的 flex-grow 和flex-shrink(更重要),让元素能够始终填满屏幕。
要习惯将容器宽度设置为百分比,而不是任何固定的值。
网页默认就是响应式的。没添加 CSS 的时候,块级元素不会比视口宽,行内元素会折行,从而避免出现水平滚动条。
加上 CSS 样式后,就需要你来维护网页的响应式特性了。这个道理说着容易做着难,而意识到每次都是从一个好的默认状态开始,有助于我们更好地实现响应式布局。
处理表格
在移动设备的流式布局里,表格的问题特别多。如果表格的列太多,很容易超过屏幕宽度(如图 8-10 所示)。
如果可以的话,建议在移动设备上用别的方式组织数据。比如将每一行数据单独用一块区域展示,让每块区域顺序叠放,或者用更适合小屏的可视化图形或者图表展示。但是,有时候就是需要用表格。
有一个办法是将表格强制显示为一个普通的块级元素,如图 8-11 所示。
这个布局由<table>、 <tr>、 <td>元素组成,但是我们对它们使用了 display: block 声明,覆盖了正常的 table、 table-row、 table-cell 的显示值。可以用 max-width 媒体查询限制在小屏下才改变表格元素的显示。 CSS 代码如下所示。(可以将代码应用到任意<table>标签查看效果。)
table {
width: 100%;
}
@media (max-width: 30em) {
table, thead, tbody, tr, th, td {
display: block; // 让表格的所有元素都显示为块级
}
thead tr {
position: absolute; //将表头移到屏幕外,将其隐藏
top: -9999px;
left: -9999px;
}
tr {
margin-bottom: 1em; // 在表格数据的每一个集合之间加上间隔
}
}
以上样式让每个单元格从上到下排列,并且在每个<tr>之间添加了外边距,但是这样会让<thead>行不再跟下面的每一列对齐,因此要用绝对定位将头部移出视口。出于可访问性的缘故,我们没有用 display: none,这样屏幕阅读器能够读到表头。虽然不是完美的解决办法,但是当其他方式失效的时候,这就是最好的方式。
响应式图片
在响应式设计中,图片需要特别关注。
不仅要让图片适应屏幕,还要考虑移动端用户的带宽限制。
图片通常是网页上最大的资源。首先要保证图片充分压缩。在图片编辑器中选择“Save for Web”选项能够极大地减小图片体积,或者用别的图片压缩工具压缩图片,比如 tinypng 网站。
还要避免不必要的高分辨率图片,而是否必要则取决于视口大小。也没有必要为小屏幕提供大图,因为大图最终会被缩小。
不同视口大小使用不同的图片
响应式图片的最佳实践是为一个图片创建不同分辨率的副本。
如果用媒体查询能够知道屏幕的大小,就不必发送过大的图片,不然浏览器为了适配图片也会将其缩小。
使用响应式技术给不同屏幕尺寸提供最合适的图片。
.hero {
padding: 2em 1em;
text-align: center;
background-image: url(coffee-beans-small.jpg); // 给移动设备提供最小的图
background-size: 100%;
color: #fff;
text-shadow: 0.1em 0.1em 0.3em #000;
}
@media (min-width: 35em) {
.hero {
padding: 5em 3em;
font-size: 1.2rem;
background-image: url(coffee-beans-medium.jpg); // 给中等屏幕提供稍大的图
}
}
@media (min-width: 50em) {
.hero {
padding: 7em 6em;
background-image: url(coffee-beans.jpg); // 给大屏幕提供完整分辨率的图
}
}
在不同屏幕的浏览器上加载这样的网页,根本看不出有什么区别。这就是关键所在。
在小断点下,屏幕尺寸不够宽,反正显示不了完整分辨率的图,但是能节省几百 KB 的流量。
在图片较多的网页上,累计节省的流量就能够显著提升网页加载速度。
使用srcset提供对应的图片
媒体查询能够解决用 CSS 加载图片的问题,但是 HTML 里的<img>标签怎么办呢?
对于这种行内图片,有另一个重要的解决方法: srcset 属性(“source set”的缩写)。
这个属性是 HTML 的一个较新的特性。
它可以为一个<img>标签指定不同的图片 URL,并指定相应的分辨率。
浏览器会根据自身需要决定加载哪一个图片。
<img alt="A white coffee mug on a bed of coffee beans"
src="coffee-beans-small.jpg" // 给不支持 srcset 的浏览器提供常规的 src 属性(比如 IE 和 Opera Mini)
srcset="coffee-beans-small.jpg 560w, // 每个图片的 URL和它的宽度
coffee-beans-medium.jpg 800w,
coffee-beans.jpg 1280w">
现在大多数浏览器支持 srcset。不支持的浏览器会根据 src 属性加载相应的 URL。这种 方式允许针对不同的屏幕尺寸优化图片。更棒的是,浏览器会针对高分辨率的屏幕做出调整。如果设备的屏幕像素密度是 2 倍,浏览器就会相应地加载更高分辨率的图片。
提示:图片作为流式布局的一部分,请始终确保它不会超过容器的宽度。为了避免这种情况发生,一劳永逸的办法是在样式表加入规则 img { max-width: 100%; }。
模块化CSS
这一部分将展示如何组织 CSS 代码,使其更易于理解和维护。
修改现有样式的时候,受影响的页面和元素是不确定的。
要怎么确保修改的影响范围和预期一致呢?怎样才能不影响我们不想修改的那些元素?
模块化 CSS( Modular CSS)是指把页面分割成不同的组成部分,这些组成部分可以在多种上下文中重复使用,并且互相之间没有依赖关系。
最终目的是,当我们修改其中一部分 CSS 时,不会对其他部分产生意料之外的影响。
之前的样式表可以使用选择器在页面上随意修改,模块化的样式则允许开发人员添加一些限制。
我们把样式表的每个组成部分称为模块( module),每个模块独立负责自己的样式,不会影响其他模块内的样式。也就是说,在 CSS 里引入了软件封装的原则。
说明:封装( encapsulation) ——相关的函数和数据集合在一起组成对象,通常用来隐藏结构化对象内部的状态或值,从而使外部因素不能操作对象内部。
CSS 中没有数据和传统函数的概念,但是有选择器及其命中的页面元素。为了达到封装的目的,这些会成为模块的组成部分,并且每个模块都只负责少量的 DOM 元素的样式。
有了封装的思想,我们就可以为页面上那些彼此分立的组件定义模块了,像导航菜单、对话框、进度条、缩略图,等等。可以通过为 DOM 元素设置一个独一无二的的类名来识别每个模块。
同时,每个模块包含一系列子元素,构建成页面上的组件。
模块内部可以嵌套其他模块,最终构成完整的页面。
基础样式
开始写模块化样式之前,需要先配置好环境。每个样式表的开头都要写一些给整个页面使用的通用规则,模块化 CSS 也不例外。
这些规则通常被称为基础样式,其他的样式是构建在这些基础样式之上的。
基础样式本身并不是模块化的,但它会为后面编写模块化样式打好基础。
基础样式应该是通用的,只添加那些影响页面上大部分或者全部内容的样式。
选择器不应该使用类名或者 ID 来匹配元素,应只用标签类型或者偶尔用用伪类选择器。
核心思想是这些基础样式提供了一些默认的渲染效果,但是之后可以很方便地根据需要覆盖基础样式。
基础样式配置完成以后,很少会再修改。
我们会在基础样式的稳定表现之上,构建模块化CSS。在样式表中,基础样式后面的内容将主要由各种模块组成。
一个简单的模块
.message {
padding: 0.8em 1.2em;
border-radius: 0.2em;
border: 1px solid #265559;
color: #265559;
background-color: #e0f0f2;
}
我们写过的代码里有很多是符合模块化 CSS 的原则的,只是之前没有注意罢了。
模块的选择器由单个类名构成,这非常重要。
选择器里没有其他规则来约束这些样式仅作用在页面上的某个地方。
对比一下,如果使用一个类似于#sidebar .message 的选择器,就意味着这个模块只能用在#sidebar 元素内部。
没有这些约束,模块就可以在任意上下文中重复使用。
通过给元素添加类名,就可以把这些样式复用到很多场景
模块的变体
保持一致性确实不错,但有时候需要特意避免一致。上面的消息模块很好用,但某些情况下我们需要它看起来有些不同。比如,我们需要显示一条报错的消息,这时候应该使用红色而不是之前的蓝绿色。再比如,我们可能想要区分传递信息的消息和表示操作成功的通知(比如保存成功)。这可以通过定义修饰符( modifiers)来实现。
通过定义一个以模块名称开头的新类名来创建一个修饰符。
例如,消息模块的 error 修饰符应该叫作 message-error。通过包含模块名称,可以清楚地表明这个类属于消息模块。
.message {
padding: 0.8em 1.2em;
border-radius: 0.2em;
border: 1px solid #265559;
color: #265559;
background-color: #e0f0f2;
}
.message--success {
color: #2f5926;
border-color: #2f5926;
background-color: #cfe8c9;
}
.message--warning {
color: #594826;
border-color: #594826;
background-color: #e8dec9;
}
.message--error {
color: #59262f;
border-color: #59262f;
background-color: #e8c9cf;
}
双连字符的写法很容易区分哪部分是模块名称,哪部分是修饰符。
不要使用依赖语境(特定位置)的选择器
假设我们正在维护一个网站,里面有浅色调的下拉菜单。有一天老板说,网页头部的下拉菜单需要改成带白色文本的深色调。
如果没有模块化 CSS,我们可能会使用类似于.page-header .dropdown 的选择器,先选中要修改的下拉菜单,然后通过选择器写一些样式,覆盖 dropdown 类提供的默认颜色。现在要写模块化 CSS,这样的选择器是严格禁用的。
虽然使用后代选择器可以满足当下的需要,但接下来可能会带来很多问题。下面我们来分析一下。
- 我们必须考虑把这段代码放在哪里,是和网页头部的样式放在一起,还是跟下拉菜单的样式放在一起?如果我们添加太多类似的单一目的的规则,样式之间毫无关联,到最后样式表会变得杂乱无章。并且,如果后面需要修改样式,你还能想起来它们放在哪里吗?
- 这种做法提升了选择器优先级。当下次需要修改代码的时候,我们需要满足或者继续提升优先级。
- 后面我们可能需要在其他场景用到深色的下拉列表。刚才创建的这个下拉列表是限定在网页头部使用的。如果侧边栏也需要同样的下拉列表,我们就得为该规则集添加新的选择器来匹配两个场景,或者完整地复制一遍样式。
- 重复使用这种写法会产生越来越长的选择器,将 CSS 跟特定的 HTML 结构绑定在一起。例如,如果有个#products-page .sidebar .social-media div:first-child h3这样的选择器,样式集就会和指定页面的指定位置紧紧耦合。
这些问题是开发人员处理 CSS 的时候遭受挫折的根源。使用和维护的样式表越长,情况越糟。
新样式需要覆盖旧样式时,选择器优先级会持续提升。到后面不知不觉地就会发现,我们写了一个选择器,其中包含两个 ID 和五个类名,只是为了匹配一个复选框。
在样式表中,元素被各种彼此不相关的选择器匹配,这样很难找到它使用的样式。理解整个样式表的组织方式变得越来越困难,你搞不明白它是怎样把页面渲染成这样的。搞不懂代码就意味着 bug 变得常见,可能很小的改动就会弄乱大片的样式。删除旧代码也不安全,因为你不了解这段代码是干什么的,是否还在用。样式表越长,问题就愈发严重。模块化 CSS 就是要尝试解决这些问题。
当模块需要有不同的外观或者表现的时候,就创建一个可以直接应用到指定元素的修饰符类。
比如,写.dropdown--dark,而不是写成.page-header .dropdown。通过这种方式,模块本身,并且只能是它本身,可以决定自己的样式表现。
其他模块不能进入别的模块内部去修改它。这样一来,深色下拉列表并没有绑定到深层嵌套的 HTML 结构上,也就可以在页面上需要的地方随意使用。
千万不要使用基于页面位置的后代选择器来修改模块。坚决遵守这个原则,就可以有效防止样式表变成一堆难以维护的代码。
多元素模块
之前讲解的模块都是由单元素(html元素)组成,但是有很多模块需要多个元素。
<div class="media">
<img class="media__image" src="runner.png">
<div class="media__body">
<h4>Strength</h4>
<p>
Strength training is an important part of
injury prevention. Focus on your core—
especially your abs and glutes.
</p>
</div>
</div>
.media {
padding: 1.5em;
background-color: #eee;
border-radius: 0.5em;
}
.media::after {
content: "";
display: block;
clear: both;
}
.media__image {
float: left;
margin-right: 1.5em;
}
.media__body {
overflow: auto;
margin-top: 0;
}
.media__body > h4 {
margin-top: 0;
}
你会发现并不需要使用很多后代选择器。图片是媒体模块的一个子元素,所以可以使用选择器.media > .media__image,但这不是必要的。因为 media__image类名包含了模块的名称,所以已经确保模块名称是独一无二的了。
正文标题确实直接使用了后代选择器。其实也可以用 media__title 类(或者 media__body__title,这样可以完整地表示出在整个层级中的位置),但是大部分时候没必要。在本例中, <h4>标签已经足够语义化,能够表明这是媒体模块的标题。不过这样一来,标题就不能使用其他的 HTML 标签( <h3>或者<h5>)了。如果你不太喜欢这么严格的限制,可以改成使用类名来匹配元素。
避免在模块选择器中使用通用标签名
我们在模块中使用了选择器.media__body > h4 来匹配标题元素。这么做是允许的,因为<h4>标签就是用来标识一个次要标题的。同样的方式也可以用在带列表的模块上。相比为列表里的每个项目都添加 menu__item 类名,使用.menu > li 匹配菜单项简单多了,尽管这种写法有些争议。
我们应该避免使用基于通用标签类型的匹配,比如 div 和 span。类似于.page-header >span 的选择器太宽泛了。
最初建立模块的时候,可能只是用 span 标签做一件事,但谁也说不准以后会不会出于其他目的再添加第二个 span。
后面再为 span 追加类名就比较麻烦了,因为我们需要在 HTML 标记中找到所有用到模块的地方,全部改一遍。
把模块组合成更大的结构
每个模块应该只做一件事情。
消息模块的职责是使消息提示醒目;媒体模块的职责是在一段文本中配置一张图片。
我们可以简洁明了地概括出它们的目标。有的模块是为了版面布局,有的是为了编写体例。
当模块想要完成不止一件事的时候,我们应该考虑把它拆分成更小的模块。
创建模块之前应该先自问一下:“从更高的层面上看,这个模块的职责是什么?”对于本例,你的回答可能是这样的:“用按钮触发下拉菜单并展示上下堆叠排列的菜单项。”
就这个场景来说,这还算是个比较恰当的描述。但是我有一条经验:“如果你不得不使用并(或者和)这个词来表述模块的职责,那你可能正在描述多项职责。”因此,模块究竟是要触发菜单,还是展示堆叠菜单项呢?
当我们需要使用并(或者和)来描述模块职责的时候,思考一下是不是在描述两种职责。
有可能不是,我的经验也不是金科玉律。但如果是的话,我们就需要为每个职责分别定义模块。这是模块封装的一个非常重要的原则,我们把它叫作单一职责原则( SingleResponsibility Principle)。尽可能把多种功能分散到不同的模块中,这样每个模块就可以保持精炼、聚焦,并且容易理解。
工具类
有时候,我们需要用一个类来对元素做一件简单明确的事,比如让文字居中、让元素左浮动,或者清除浮动。这样的类被称为工具类( utility class)。
.text-center {
text-align: center !important;
}
.float-left {
float: left;
}
.clearfix::before,
.clearfix::after {
content: " ";
display: table;
}
.clearfix::after {
clear: both;
}
.hidden {
display: none !important;
}
这里用到了两次!important。工具类是唯一应该使用 important 注释的地方。
事实上,工具类应该优先使用它。这样的话,不管在哪里用到工具类,都可以生效。我敢肯定,任何时候为元素添加 text-center 类,都是想让文本居中,不想让其他样式覆盖它。用了 important 注释就可以确保这一点。
工具类的作用立竿见影。在页面上做点小事儿的时候不需要创建一个完整的模块,这种情况下可以用一个工具类来实现。但是不要滥用工具类。对于大部分网站,最多十几个工具类就够用了。
提示:JavaScript 替代方案
在大型团队里书写模块化样式,需要一些苛刻的约束条件来确保每个人遵守相同的约定。
同时也需要采取一些措施来防止大家新建的模块名称出现冲突。
为了解决这些问题,一些 Web开发社区开始尝试模块化 CSS 的替代方案。
一番探索后,他们转向了 JavaScript,最终发明了一种解决方案,被称为内联样式( inline styles)或者 CSS in JS。
这种方案不再依赖类命名的口头约定,而是使用 JavaScript 来控制,要么生成独一无二的类名,要么使用 HTML 的 style 属性引入所有的样式。
已经出现了不少具备这种功能的JavaScript 库,绝大部分库绑定了一个 JavaScript 框架或者工具集,比如 WebPack。
对比、颜色和间距
间距
思考一下行高
在盒模型中,元素的内容盒子为内边距所环绕,然后是边框,最后是外边距。
但是对于段落( p标签 )和标题( h* 标签)这样的元素,内容盒子并不是只有显示出来的文字区域,元素的行高决定了内容盒子最终的高度,这要超出字符的顶部和底部。
如图 12-18 所示,文字高度是 1em,但是行高超出了文字上下边缘一点。
网页里的行高是 1.4,这是从<body>元素上设置并继承下来的默认行高。
这样只有一行文字的元素,内容盒子的高度就是 1.4em,文字在内部垂直居中。字号是 16px,内容盒子的最终高度就是 22.4px。额外的 6.4px 平均分配到文字的上方和下方。
因此,如果为标题设置 30px 的底部外边距,那么外边距的顶部和标题文字之间实际上会多出来 3.2px。
外边距的下方,段落的内容盒子同样会多出来 3.2px(超出的间距是一样的,因为标题和段落有同样的行高和字号)。
这样就会导致标题文字和段落文字之间的实际间隔是 36.4px。
为行内元素设置间距
文本行的高度是由行高决定的。
如果为行内元素( inline )添加内边距,元素本身会变高,却不会增加文本行的高度。文本行的高度始终只由行高来决定。
只有行内元素有这种行为。
如果一个元素是弹性子元素(或者行内块级元素),为了容纳它,其所在的行会随之增高。
排版
网页设计,成也字体,败也字体。几年前, Web 开发者只能从有限的一些字体中做选择,即所谓的 Web 安全字体( Web safe font)。这些字体类型包括 Arial、 Helvetica、 Georgia 等,大部分用户的系统会安装。因为浏览器只能使用这些系统字体渲染页面,所以我们必须使用它们。我们可以指定非系统字体,比如 Helvetia Neue,但只有那些碰巧安装了这款字体的用户才能正确显示,其他用户只能看到通用的回退方案。
随着 Web 字体的兴起,情况改变了。 Web 字体使用@font-face 规则,告诉浏览器去哪里找到并下载自定义字体,供页面使用。原本平淡无奇的页面,使用自定义字型之后,可能就改观了。
Web 字体
通过在线服务使用 Web 字体是最简单也最普遍的方式。常见的如下所示。
- Typekit
- Webtype
- 谷歌字体
无论收费还是免费,这些服务都为你解决了很多问题,包括技术上(托管服务)和法律上(授权许可)的一些问题。它们都提供了可以选择字体的大型字体库,但有时如果你需要某些特定字体,可能需要开通特定服务才可以使用。
说明:字型( typeface)和字体( font)这两个术语经常被混为一谈。
字型通常是指字体(比如 Roboto)的整个家族,一般由同一个设计师创造。一种字型可能会存在多种变体和字重(比如细体、粗体、斜体、压缩,等等),这些变体的每一种可称之为一种字体( font)。
使用web字体
Web 字体服务提供的服务,要么提供所需的 CSS URL 地址,要么提供可以为网页添加 CSS 的 JavaScript片段。
以 URL 地址为例,在<link>标签中添加URL地址,并添加到页面的<head>里,这样就为页面添加了一个包含字体描述的样式。表。
<link href="https://fonts.googleapis.com/css?family=Roboto:300|Sansita:800" rel="stylesheet">
还需要使用 font-family 属性来指定 Roboto 或者 Sansita,才能使用字体。
在标签中为正文字体设置 Roboto,整个网页会继承使用。
因为页面中添加了字体样式表,所以浏览器可以理解这些字体名称指向下载的 Web 字体,并将其应用到页面。
body {
margin: 0;
font-family: Roboto, sans-serif;
line-height: 1.4;
background-color: var(--extra-light-gray);
}
h1, h2, h3, h4 {
font-family: Sansita, serif;
}
/* ... */
.home-link {
color: var(--text-color);
font-size: 1.6rem;
font-family: Sansita, serif;
font-weight: bold;
text-decoration: none;
}
如何使用@font-face
提供字体服务的网站把添加字体的工作做得如此简单易用,但我们依然需要了解一下它们是怎么实现的。
先来看看谷歌提供的 CSS 文件。在浏览器中打开 URL https://fonts.googleapis.com/css? family=Roboto:300|Sansita:800,就可以看到谷歌的 CSS。
/* latin */
@font-face { //每条@font-face 规则定义一个字体,可以在页面里其他 CSS 中使用
font-family: 'Roboto'; //声明字体名称
font-style: normal; //设置这条@font-face规则使用的字体样式
font-weight: 300; //设置这条@font-face规则使用的字体字重
src:local('Roboto Light'), local('Roboto-Light'),
url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fBBc4.woff2) format('woff2'); //可以找到字体文件的位置
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; //这条@font-face 规则使用的 Unicode 编码范围
}
@font-face 规则定义了浏览器字体,以便在页面的 CSS 中使用。
这里的第一条规则实际上是说,“如果页面需要渲染 font-family 为 Roboto 的拉丁字符,这些字符使用了正常的字体样式(非斜体)并且字重为 300,那么就使用这个字体文件”。
font-family 设置了引用字体的名称,可以在样式表的其他地方使用。
src:提供了一个逗号分隔的浏览器可以搜索的地址列表,以 local(Roboto Light)和 local(Roboto-Light)开头,这样的话,如果用户的操作系统中恰好安装了名为 Roboto Light 或者 Roboto-Light 的字体,就使用这些字体。否则,就下载使用 url()指定的 woff2 字体文件。
字体格式与回退处理
谷歌的样式表假设浏览器支持 WOFF2 格式的字体文件。
这是可以的,因为谷歌通过检查浏览器的用户代理字符串,能够判断出我的浏览器( Chrome)支持这些字体文件。
如果我们在 IE10浏览器中访问相同的 URL,返回的样式表稍有不同,其中会使用 WOFF 字体。
WOFF 是指 Web 开放字体格式,这是一种专为网络使用而设计的压缩字体格式。
所有的现代浏览器都支持 WOFF,但不是所有的都支持 WOFF2( WOFF2 格式有更好的压缩效果,因此文件更小)。
你应该不希望像谷歌那样,每次都去判断用户代理字符串。代码清单 13-4 展示了一种稳健的解决方案,同时提供 WOFF 和 WOFF2 字体文件的 URL(为了使代码更易读,这里使用了简写的 URL)。
@font-face {
font-family: "Roboto";
font-style: normal;
font-weight: 300;
src: local("Roboto Light"), local("Roboto-Light"),
url(https://example.com/roboto.woff2) format('woff2'), // 使用列表中支持的第一种格式
url(https://example.com/roboto.woff) format('woff'); //不支持 WOFF2 的浏览器回退为 WOFF
}
Web 字体刚刚兴起的时候,开发者必须引入四五种不同格式的字体,因为每款浏览器支持的格式都不一样。现在绝大部分浏览器已经支持 WOFF,因为 WOFF2 加载更快,一般会提供这两种字体的 URL。
同一种字型的多种变体
如果需要用到同一种字型的多种字体,那么每一种字体都需要自己的@font-face 规则。如果你在谷歌字体页面上同时选择了 Roboto 的细体和粗体版本,谷歌就会提供一个类似于https://fonts.googleapis.com/css?family=Roboto:300,700 的样式表 URL。
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300; //细体 Roboto
src: local('Roboto Light'), local('Roboto-Light'),
url(https://fonts.gstatic.com/s/roboto/v15/Hgo13ktfSpn0qi1SFdUfZBw1xU1rKptJj_0jans920.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
...
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700; // 粗体 Roboto
src: local('Roboto Bold'), local('Roboto-Bold'),
url(https://fonts.gstatic.com/s/roboto/v15/d-6IYplOFocCacKzxwXSOJBw1xU1rKptJj_0jans920.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
如果页面上需要渲染字重为 300 的 Roboto,就使用第一种定义;如果需要渲染字重为 700 的 Roboto,就使用第二种。
如果页面样式需要用到其他版本的字体(比如 font-weight: 500 或者 font-style: italic),浏览器就会从提供的两种字体中选择更接近的字体。
不过这取决于浏览器,它可能会把某个已提供字体倾斜或者加粗来达到想要的效果,通过使用几何学的方法来实现字母形状的转换。
因为这样的字体显示肯定不如原生设计的效果好,所以不建议依靠这种方式。
在你使用谷歌字体或者其他字体服务提供商时,通过界面操作就可以获得所需的代码。
有时可能服务商没有提供你要使用的字体,这种情况下就需要自己提供字体服务,使用@font-face规则定义浏览器所需的格式。
调整字距,提升可读性
现在 Web 字体加载好了,调整文本样式时。这里涉及两个属性: line-height 和 letter-spacing,这两个属性可以控制文本行之间的距离(垂直方向)和字符之间的距离(水平方向)。
如果文字间距太紧凑,阅读更多句子或者词语就会比较费劲,间距太大也会有同样的问题。
图 13-7 展示了多种不同间距的文本。
正文主体的字间距
为 line-height 和 letter-spacing 找到合适的值是件主观性很强的事情。
最好的解决办法通常是多试几个值,如果两个值要么太紧凑要么太松散,那就取它们的中间值。
幸运的是,下面介绍的这些经验法则可以为你提供帮助。
line-height 属性的初始值是关键字 normal,大约等于 1.2(确切的数值是在字体文件中编码的,取决于字体的 em 大小),但是在大部分情况下,这个值太小了。对于正文主体来说,介于 1.4 和 1.6 之间的值比较理想。
接下来,我们看看 letter-spacing。这个属性需要一个长度值,用来设置每个字符之间的距离。即使只设置 1px,也是很夸张的字间距了,因此这应该是个很小的长度值。我在尝试找到合适值的时候,一般每次只增加 1em 的 1/100(例如, letter-spacing: 0.01em)。
标题、小元素和间距
标题的间距通常和正文主体不太一样。
标题一般比较短,通常只有几个字,但有时候也会出现比较长的标题。设计的时候常犯的错误就是只测试短标题。
有时候 1.4 的行高可能显得有点大,这也取决于字型,特别是设置大字号的时候。
相对来讲,字符间距反而可以设置得稍宽一些。
恼人的 FOUT 和 FOIT
页面使用的字体文件数量应该精简到最少,但即使这样,仍然可能存在问题。
浏览器中经常会遇到这种情况,页面的内容和布局就要开始渲染了,字体却还在下载中。有必要来思考一下这时候会发生什么。
开始时,大多数的浏览器供应商为了尽可能快地渲染页面,使用了可用的系统字体。然后,一小段时间过去了, Web 字体加载完成,页面会使用 Web 字体重新渲染一次。图 13-12 阐述了这个过程。
比起系统字体, Web 字体很可能会在屏幕上占据不一样的空间。第二次渲染时,页面布局变了,文字突然跳动了。如果这是在第一次渲染之后很快发生,用户可能不会注意到。但是如果字体下载过程中有网络延迟(或者字体文件太大了),可能长达几秒之后才会再次渲染页面。这种情况发生时,有的用户可能会感到厌烦。他们可能已经开始阅读网页内容了,这时页面突然变化,会让他们注意力分散。这就是所谓的 FOUT,即无样式文本闪动( Flash of Unstyled Text)。
因为开发者们不喜欢这样,所以大部分浏览器供应商修改了浏览器的行为。他们不再渲染回退字体,改成渲染页面上除了文本以外的其他所有元素。确切地说,他们把文本渲染成不可见的,因此文字依然会占据页面的空间。通过这种方式,页面的容器元素得以实现,用户就可以看到页面正在加载。这就导致了一个新的问题, FOIT,即不可见文本闪动( Flash of Invisible Text)。如图 13-13 所示,背景颜色和边框都显示出来了,但是文字在第二次渲染的时候才显示,即 Web字体加载之后。
这种方案解决了之前的问题,但又带来了新问题。如果 Web 字体加载时间很长会发生什么?或者加载失败呢?页面会一直空白,这些彩色的盒子只是空壳,对于用户来讲完全没有意义。这种情况发生时,我们还是希望使用 FOUT 时的系统字体。
开发者针对这些问题提出了很多解决办法,基本上每一年会涌现出“更好”的方案。要解决问题,其实就是要避免发生 FOUT 和 FOIT。然而 在 Web 字体领域,这两个问题从未完全解决过。我们能做的就是尽可能让它们产生的影响降到最低。
幸好关于这些问题的讨论即将尘埃落定,这里我就不从头介绍好几种不同的技巧了,直接演示我认为最合理的解决方案。这里需要用到一点 JavaScript 来控制字体加载。
同时我也会介绍一 个即将新增的 CSS 属性,不需要 JavaScript 就可以提供这种控制。你可以使用任意一种或者同时使用两种。
使用Font Face Observer
使用 JavaScript 可以监控字体加载事件,这样就可以更好地控制 FOUT 与 FOIT 的发生过程。还可以使用 js 库来帮助处理,我喜欢用一款名叫 Font Face Observer 的库。这个库可以让你等待Web 字体加载,然后做出相应的响应。我一般是在字体准备好时,使用 JavaScript 为<html>元素添加一个 fonts-loaded 类。然后就可以使用这个类为页面设置不同的样式,用不用 Web 字体都可以。
<script type="text/javascript">
var html = document.documentElement;
var script = document.createElement("script"); // 动态创建<script>标签, 添加Font Face Observer 到页面上
script.src = "fontfaceobserver.js";
script.async = true;
script.onload = function () {
var roboto = new FontFaceObserver("Roboto"); // 为 Roboto 和 Sansita字体创建观察器
var sansita = new FontFaceObserver("Sansita");
var timeout = 2000;
Promise.all([
roboto.load(null, timeout),
sansita.load(null, timeout)
]).then(function () {
html.classList.add("fonts-loaded"); // 两种字体都加载完成以后,为<html>元素添加 fonts-loaded 类
}).catch(function (e) {
html.classList.add("fonts-failed"); // 如果字体加载失败,为<html>元素添加 fonts-failed 类
});
};
document.head.appendChild(script);
</script>
这段脚本创建了两个观察器,分别用于 Roboto 字体和 Sansita 字体。 Promise.all()方法会等待两个字体都加载完成,然后脚本为页面添加 fonts-loaded 类。如果加载失败,或者加载超时(超过两秒), catch 回调函数会被调用,为页面添加 fonts-failed 类。这样当页面加载完成时,脚本会为页面要么添加 fonts-loaded 类,要么添加 fonts-failed 类。
回退到系统字体
针对字体加载,我们可以采用两种基本方法。
第一种,在 CSS 中使用回退字体,然后在选择器中使用.fonts-loaded,把回退字体改成想要的 Web 字体。
这样就可以把浏览器的 FOIT (不可见文本)改为 FOUT(无样式文本)。
第二种,在 CSS 中使用 Web 字体,然后在选择器中使用.fonts-failed,把字体改成回退字体。
这种方法依然会产生 FOIT,但是如果超时就会转换为系统字体,页面不会在加载失败时被不可见文本卡住。
我们来实现第二种方法。以下的代码使用.fonts-failed 类添加回退样式
body {
margin: 0;
font-family: Roboto, sans-serif;
line-height: 1.4;
letter-spacing: 0.01em;
background-color: var(--extra-light-gray);
}
.fonts-failed body {
font-family: Helvetica, Arial, sans-serif; //如果 Web 字体加载失败,就回退到系统字体
}
h1, h2, h3, h4 {
font-family: Sansita, serif;
letter-spacing: 0.03em;
}
.fonts-failed h1,
.fonts-failed h2,
.fonts-failed h3,
.fonts-failed h4 {
font-family: Georgia, serif; //如果 Web 字体加载失败,就回退到系统字体
}
...
.home-link {
color: var(--text-color);
font-size: 1.6rem;
font-family: Sansita, serif;
font-weight: bold;
letter-spacing: 0.03em;
text-decoration: none;
}
.fonts-failed .home-link {
font-family: Georgia, serif; //如果 Web 字体加载失败,就回退到系统字体
}
字体加载失败时(或者加载超时), fonts-failed 类被添加到页面,回退样式就会应用到页面上。网速快时, Web 字体加载之前,会有短暂的 FOIT 出现。如果网速比较慢, FOIT 会持续两秒,然后显示回退字体。
处理字体加载没什么标准答案。如果你对站点的加载时间进行了分析,那么在决定使用哪种方法时,可以使用它来帮助你。一般来说,网速快时 FOIT 更容易接受一些,但网速慢时应该倾向于 FOUT,根据实际情况来判断。
准备使用font-display
这条属性需要在@font-face 规则内部使用,用来指定浏览器应该如何处理 Web 字体加载。
@font-face {
font-family: "Roboto";
font-style: normal;
font-weight: 300;
src: local("Roboto Light"), local("Roboto-Light"),
url(https://example.com/roboto.woff2) format('woff2'),
url(https://example.com/roboto.woff) format('woff');
font-display: swap; // 加载字体时使用 swap行为,即 FOUT
}
这告诉浏览器立即显示回退字体,然后等 Web 字体可用的时候进行交换( swap),简而言之,就是 FOUT。
- auto——默认行为(在大多数浏览器中是 FOIT)。
- swap——显示回退字体,在 Web 字体准备好之后进行交换( FOUT)。
- fallback——介于 auto 和 swap 之间。文本会保持较短时间( 100ms)的隐藏状态,如果这时候 Web 字体还没有准备好,就显示回退字体。接下来一旦 Web 字体加载完成,就会显示 Web 字体。
- optional——类似于 fallback,但是允许浏览器基于网速判断是否显示 Web 字体。这就意味着在较慢的连接条件下 Web 字体可能不会显示。
这些选项比之前的几行 JavaScript 提供了更多的控制能力。对于高网速, fallback 表现最好,会出现短暂的 FOIT,但如果 Web 字体加载超过了 100ms 就会产生 FOUT。对于低网速, swap更好一些,可以立刻渲染回退字体。如果 Web 字体对于整体设计来讲并非必不可少的时候,就可以使用 optional。