Skip to content

相对单位

说起给属性指定值,CSS提供了很多选项。人们最熟悉同时也最简单的应该是像素单位(px)。它是绝对单位,即 5px 放在哪里都一样大。
而其他单位,如 em 和 rem,就不是绝对单位,而是相对单位。相对单位的值会根据外部因素发生变化。
比如,2em 的具体值会根据它作用到的元素(有时甚至是根据属性)而变化。因此相对单位的用法更难掌握。

开发人员,即便是经验丰富的 CSS 开发人员,通常也不愿意使用相对单位,包括经常提到的 em。
em 值变化的方式使其难以预测,不如像素简单明了。

相对单位可以为我们所用,用得恰当的话,它们会让代码更简洁、更灵活,也更简单。

相对值的好处

CSS为网页带来了后期绑定(late-binding)的样式:直到内容和样式都完成了,二者才会结合起来。
这会给设计流程增加复杂性,而这在其他类型的图形设计中是不存在的。不过这也带来了好处,即一个样式表可以作用于成百上千个网页。
此外,用户还能直接改变最终的渲染效果,比如用户可以改变默认字号或者缩放浏览器窗口。

在早期的计算机应用开发程序(以及传统的出版行业)中,开发人员(或者出版商)明确知道其媒介的限制。
一个典型的程序窗口可能宽 400px、高 300px,一个页面可能是宽 4 英寸(1 英寸约合 2.54 厘米)、高 6.5 英寸。
因此,当开发人员设置应用程序的按钮和文字布局时,他们能精确地知道元素在屏幕上的大小和留给其他元素的空间。在网页上,一切都变了。

那些年追求的像素级完美

在Web环境下,用户可以设置浏览器窗口的大小,而CSS必须适应这种窗口大小。此外,当网页打开后,用户还可以缩放网页,CSS还需要适应新的限制。
也就是说,不能在刚创建网页时就应用样式,而是等到要将网页渲染到屏幕上时,才能去计算样式。

这给 CSS 增加了一个抽象层。我们无法根据理想的条件给元素添加样式,而是要设置无论元素处于任意条件,都能够生效的规则。
现在的 Web 环境下,网页需要既可以在 4 英寸的手机屏幕上渲染,也可以在 30 英寸的大屏幕上渲染。

在很长时间里,网页设计者通过聚焦到“像素级完美”的设计来降低这种复杂性。
他们会创建一个紧凑的容器,通常是居中的一栏,大约 800px 宽。然后再像之前的本地应用程序或者印刷出版物那样,在这些限制里面进行设计。

像素级完美的时代终结了

随着技术的发展,加上制造商推出高清显示器,像素级完美的方式逐渐走向了终点。
在21世纪初,很多人开始讨论是否可以安全地将网页宽度设计成 1024px,而不是 800px。
随后,人们又开始讨论同样的话题,是否要将网页宽度设计成 1280px。当时我们得做出选择,到底是让网页宽于旧计算机,还是窄于新计算机。

等到智能手机出现后,开发人员再也无法假装每个用户访问网站的体验都能一样。
不管我们喜欢与否,都得抛弃以前那种固定宽度的栏目设计,开始考虑响应式设计。我们无法逃避CSS带来的抽象性。我们得拥抱它。

响应式——在 CSS 中指的是样式能够根据浏览器窗口的大小有不同的“响应”。
这要求有意地考虑任何尺寸的手机、平板设备,或者桌面屏幕。

CSS带来的抽象性也带来了额外的复杂性。如果给一个元素设置800px的宽度,在小窗口下会是什么样?
水平菜单如果无法在一行显示会是什么样?在写CSS的时候,我们既要考虑整体性,也要考虑差异性。
当有很多方法解决同一个问题时,我们要选择能够兼顾更多情况的方法。

相对单位就是CSS用来解决这种抽象的一种工具。
我们可以基于窗口大小来等比例地缩放字号,而不是固定为14px,或者将网页上的任何元素的大小都相对于基础字号来设置,
然后只用改一行代码就能缩放整个网页。
下面来看看CSS是如何实现这些功能的。

像素、点、派卡

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 等于当前元素的字号,其准确值取决于作用的元素。下图是一个内边距为 1em 的 div 元素。

内边距为1em的元素

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!doctype html>
<head>
  <style>
  .padded {
    font-size: 16px;
    /* 设置四个内边距为 font-size */
    padding: 1em;
  }
  </style>
</head>

<body>
  <div class="padded">
    We have built partnerships with small farms around the world to
    hand-select beans at the peak of season. We then carefully roast in <a href="/batches">small batches</a> to maximize their potential.
  </div>
</body>

规则集指定了字号为16px,也就是元素局部定义的1em。然后使用em指定了元素的内边距。

这里设置内边距的值为1em。浏览器将其乘以字号,最终渲染为16px。
这一点很重要:浏览器会根据相对单位的值计算出绝对值,称作计算值(computed value)。

在本例中,设置内边距为2em,会产生一个32px的计算值。
如果另一个选择器也命中了相同的元素,并修改了字号,那么就会改变 em 的局部含义,计算出来的内边距也会随之变化。

当设置 padding、height、width、border-radius 等属性时,使用 em 会很方便。
这是因为当元素继承了不同的字号,或者用户改变了字体设置时,这些属性会跟着元素均匀地缩放。

下图展示了两个不同大小的盒子,它们的字号、内边距和圆角都会不一样。

元素的内边距和圆角都是相对值

在定义这些盒子的样式时,可以用em指定内边距和圆角。
给每个元素设置1em的内边距和圆角,再分别指定不同的字号,那么这些属性会随着字体一起缩放。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!doctype html>
<head>
  <style>
  /* keep the spans from bleeding off the top of the page */
  body {
    margin: 1.5em;
  }

  .box {
    padding: 1em;
    border-radius: 1em;
    background-color: lightgray;
  }

  .box-small {
    font-size: 12px;
  }

  .box-large {
    font-size: 18px;
  }
  </style>
</head>

<body>
  <span class="box box-small">Small</span>
  <span class="box box-large">Large</span>
</body>

这段代码用em定义了一个盒子,同时定义了一个small和一个large的修饰符,分别指定不同的字号。

这就是em的好处。可以定义一个元素的大小,然后只需要改变字号就能整体缩放元素。
稍后会再举一个例子,在此之前,我们先说说em和字号。

使用 em 定义字号

谈到font-size属性时,em表现得不太一样。
之前提到过,当前元素的字号决定了em。但是,如果声明 font-size: 1.2em,会发生什么呢?
一个字号当然不能等于自己的1.2倍。实际上,这个 font-size 是根据继承的字号来计算的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!doctype html>
<head>
  <style>
  body {
    font-size: 16px;
  }

  .slogan {
    font-size: 1.2em;
  }
  </style>
</head>

<body>
  We love coffee
  <p class="slogan">We love coffee</p>
</body>

使用em定义两种不同的字号

第一行文字在<body>标签中,因此它会按照body的字号来渲染。第二段的slogan继承了这个字号。

简单起见,这里用像素单位。接下来使用em来放大slogan的字号。

slogan的指定字号是 1.2em。为了得到计算的像素值,需要参考继承的字号,即 16px。因为 16×1.2 = 19.2,所以计算值为 19.2 px。

提示
如果知道字号的像素值,但是想用em声明,可以用一个简单的公式换算:用想要的像素大小除以父级(继承)的像素字号。
比如,想要一个 10px 的字体,元素继承的字体是 12px,则计算结果是 10/12 = 0.8333em
如果想要一个 16px 的字体,父级字号为 12px,则计算结果是 16/12 = 1.3333em

了解这些非常有用。对大多数浏览器来说,默认的字号为 16px。准确地说,medium 关键字的值是 16px。

em 同时用于字号和其他属性

现在你已经用em定义了字号(基于继承的字号),而且也用 em 定义了其他属性,比如 padding 和 border-radius(基于当前元素的字号)。
em的复杂之处在于同时用它指定一个元素的字号和其他属性。这时,浏览器必须先计算字号,然后使用这个计算值去算出其余的属性值。
这两类属性可以拥有一样的声明值,但是计算值不一样。

在前面的例子里,字号的计算值为 19.2px(继承值 16px 乘以 1.2em)。
下图展示了相同的 slogan 元素,但是内边距为 1.2em,背景为灰色,这样能明显地看到内边距的大小。
内边距比字号稍微大一些,尽管它们的声明值相同。

字号为1.2em和内边距为1.2em的元素

这是因为该段落从 body 继承了16px的字号,最终字号的计算值为 19.2px。因此 19.2px 是em的局部值,用于计算内边距

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<!doctype html>
<head>
  <style>
  body {
    font-size: 16px;
  }

  .slogan {
    /* 计算值为 19.2px */
    font-size: 1.2em;
    /* 计算值为 23.04px */
    padding: 1.2em;
    background-color: #ccc;
  }
  </style>
</head>

<body>
  <p class="slogan">We love coffee</p>
</body>

在这个例子里,padding 的声明值为 1.2em,乘以 19.2px(当前元素的字号),得到计算值为 23.04px。
尽管 font-size 和 padding 的声明值相同,计算值却不一样。

字体缩小的问题

当用 em 来指定多重嵌套的元素的字号时,就会产生意外的结果。
为了算出每个元素的准确值,就需要知道继承的字号,如果这个值是在父元素上用em定义的,就需要知道父元素的继承值,以此类推,就会沿着DOM树一直往上查找。

当使用em给列表元素定义字号并且多级嵌套时,这个问题就显现出来了。
绝大部分Web开发人员曾遇到过类似于下图的现象。文字缩小了!正是这种问题让开发人员惧怕使用 em。

嵌套列表的文字缩小了

当列表多级嵌套并且给每一级使用em定义字号时,就会发生文字缩小的现象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!doctype html>
<head>
  <style>
    body {
      font-size: 16px;
    }

    ul {
      font-size: 0.8em;
    }
  </style>
</head>

<body>
  <ul>
  <li>Top level
    <ul>
      <li>Second level
        <ul>
          <li>Third level
            <ul>
              <li>Fourth level
                <ul>
                  <li>Fifth level</li>
                </ul>
              </li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
</ul>
</body>

设置无序列表的字号为 0.8em。选择器选中了网页上每个<ul>元素,因此当这些列表从其他列表继承字号时,em就会逐渐缩小字号。

每个列表元素的字号等于 0.8 乘以其父元素的字号。
算出来第一级列表的字号为 12.8px,第二级缩小到 10.24px(12.8px × 0.8),第三级缩小到 8.192px,以此类推。

同理,如果指定一个大于1em的字号,文字会逐渐增大。
我们想要的是指定顶部的字号,然后保持子级的字号一致

文字大小正确的嵌套列表

1
2
3
4
5
6
7
8
ul {
    font-size: 0.8em;
}

/* 嵌套的列表应当跟其父级的字号一致 */
ul ul {
    font-size: 1em;
}

第二个选择器选中了嵌套在某个无序列表中的所有无序列表,也就是除了顶级列表以外的其他列表。嵌套列表的字号等于其父级的字号

这样确实解决了问题,尽管这个方式不完美。
设置一个值,然后马上用另一个规则覆盖。如果不用提升选择器的优先级来覆盖规则,就更好了。

这些例子告诉我们,如果不小心的话,em就会变得难以驾驭。
em用在内边距、外边距以及元素大小上很好,但是用在字号上就会很复杂。值得庆幸的是,我们有更好的选择:rem。

使用 rem 设置字号

当浏览器解析HTML文档时,会在内存里将页面的所有元素表示为DOM(文档对象模型)。
它是一个树结构,其中每个元素都由一个节点表示。<html> 元素是顶级(根)节点。它下面是子节点,<head><body>
再下面是逐级嵌套的后代节点。

在文档中,根节点是所有其他元素的祖先节点。
根节点有一个伪类选择器(:root),可以用来选中它自己。这等价于类型选择器html,但是html的优先级相当于一个类名,而不是一个标签。

rem 是 root em 的缩写。rem不是相对于当前元素,而是相对于根元素的单位。
不管在文档的什么位置使用 rem, 1.2rem 都会有相同的计算值:1.2 乘以根元素的字号
下面代码先指定了根元素的字号,然后用 rem 定义了无序列表的相对字号。

1
2
3
4
5
6
7
8
/* :root 伪类等价于类型选择器 html,使用浏览器的默认字号 16px */
:root {
    font-size: 1em;
}

ul {
    font-size: .8rem;
}

在这个例子里,根元素的字号为浏览器默认的字号 16px(根元素上的em是相对于浏览器默认值的)。
无序列表的字号设置为 0.8rem,计算值为 12.8px。因为相对根元素,所以所有字号始终一致,就算是嵌套列表也一样。

可访问性:对字号使用相对单位

有些浏览器给用户提供了两种方式来设置文字大小:缩放操作和设置默认字号。按住 Ctrl+Ctrl−,用户可以缩放网页。
这种操作会缩放所有的字和图片,让网页整体放大或者缩小。在某些浏览器中,这种改变只会临时对当前标签页生效,不会将缩放设置带到新的标签页。

设置默认字号则不一样。不仅很难找到设置默认字号的地方(通常在浏览器的设置页),而且用这种方式改变字号会永久生效,除非用户再次修改默认值。
这种方式的缺点是,它不会影响用 px 或者其他绝对单位设置的字号。
由于默认的字号对某些用户而言很重要,尤其是对视力受损的人,所以应该始终用相对单位或者百分比设置字号。

与 em 相比,rem 降低了复杂性。实际上,rem 结合了 px 和 em 的优点,既保留了相对单位的优势,又简单易用。
那是不是应该全用rem,抛弃其他选择呢?答案是否定的。

在 CSS 里,答案通常是“看情况”。rem 只是你工具包中的一种工具。
掌握CSS很重要的一点是学会在适当的场景使用适当的工具。
我一般会用 rem 设置字号,用 px 设置边框,用 em 设置其他大部分属性,尤其是内边距、外边距和圆角(不过我有时用百分比设置容器宽度)。

这样字号是可预测的,同时还能在其他因素改变元素字号时,借助em缩放内外边距。
用px定义边框也很好用,尤其是想要一个好看又精致的线时。
这些是我在设置各种属性时常用的单位,但它们仅仅是工具,在某些情况下,用其他工具会更好。

提示
拿不准的时候,用rem设置字号,用px设置边框,用em设置其他大部分属性。

停止像素思维

过去几年有一个常见的模式,更准确地说是反模式,就是将网页根元素的字号设置为 0.625em 或者 62.5%

1
2
3
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可能需要反复练习,但这一切是值得的。

这并不意味着永远不能用像素。如果是跟设计师沟通,可能就需要讨论具体的像素值,这没问题。
在项目之初,需要确定基本的字号(通常是标题和脚注的常用字号)。讨论大小的时候用绝对值更简单。

将大小转换成rem需要计算,记得随手带一个计算器。
给根元素设置了字号后,就定义了一个rem。在这之后,应该只在少数特殊情况下使用像素,而不能经常使用。

设置一个合理的默认字号

如果你希望默认字号为 14px,那么不要将默认字体设置为10px然后再覆盖一遍,而应该直接将根元素字号设置为想要的值。
将想要的值除以继承值(在这种情况下为浏览器默认值)是 14/16,等于 0.875

1
2
3
:root {
    font-size: 0.875em;
}

现在你已经给网页设置了想要的字号,不用在其他地方再指定一遍了。你只需要相对它去修改其他元素(比如标题)的字号。

接下来创建一个如图所示的面板。基于根元素的14px字号,用相对单位来构建这个面板。

使用相对单位和一个继承字号创建的面板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!doctype html>
<head>
  <style>
    :root {
      font-size: 0.875em;
    }

    body {
      font-family: Helvetica, Arial, sans-serif;
    }

    .panel {
      padding: 1em;
      border: 1px solid #999;
      border-radius: 0.5em;
    }
    .panel > h2 {
      margin-top: 0;
      font-size: 0.8rem;
      font-weight: bold;
      text-transform: uppercase;
    }
  </style>
</head>

<body>
  <div class="panel">
    <h2>Single-origin</h2>
    <div class="panel-body">
      We have built partnerships with small farms around the world to
      hand-select beans at the peak of season. We then carefully roast
      in <a href="/batch-size.html">small batches</a> to maximize their
      potential.
    </div>
  </div>
</body>

这里用em设置内边距和圆角,用rem设置标题的字号,用px设置边框。

代码给面板的四周加上了细边,并给标题指定了样式。
我创建了一个较小的标题,同时将字体加粗,全大写。(如果设计需要的话,可以改成更大的字号,或者其他字体。)

第二个选择器里的 > 是一个直接后代组合器。它选中了 .panel 元素的一个 h2 子元素。
给面板主体添加 panel-body 类只是为了明确含义,在 CSS 中并未用到。
因为这个元素已经继承了根元素的字号,所以它看起来就是理想的样子,不需要覆盖。

构造响应式面板

更进一步地说,我们甚至可以根据屏幕尺寸,用媒体查询改变根元素的字号。
这样就能够基于不同用户的屏幕尺寸,渲染出不同大小的面板

不同屏幕尺寸下的响应式面板: 300px(左上)、800px(右上)、1440px(下):

不同屏幕尺寸下的响应式面板

媒体查询,即 @media 规则,可以指定某种屏幕尺寸或者媒体类型(比如,打印机或者屏幕)下的样式。
这是响应式设计的关键部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!doctype html>
<head>
  <style>
    :root {
      font-size: 0.75em;
    }
    /* 仅作用到宽度 800px 及其以上的屏幕,覆盖之前的值 */
    @media (min-width: 800px) {
      :root {
        font-size: 0.875em;
      }
    }
    /* 仅作用到宽度 1200px 及其以上的屏幕,覆盖前面两个值 */
    @media (min-width: 1200px) {
      :root {
        font-size: 1em;
      }
    }

    body {
      font-family: Helvetica, Arial, sans-serif;
    }

    .panel {
      padding: 1em;
      border: 1px solid #999;
      border-radius: 0.5em;
    }
    .panel > h2 {
      margin-top: 0;
      font-size: 0.8rem;
      font-weight: bold;
      text-transform: uppercase;
    }
  </style>
</head>

<body>
  <div class="panel">
    <h2>Single-origin</h2>
    <div class="body">
      We have built partnerships with small farms around the world to
      hand-select beans at the peak of season. We then carefully roast
      in <a href="/batch-size.html">small batches</a> to maximize their
      potential.
    </div>
  </div>
</body>

第一个规则集指定了一个较小的默认字号,这是希望在小屏幕上显示的字号。
然后使用媒体查询覆盖该值,在800px、1200px以及更大的屏幕上逐渐增大字号。

通过给页面根元素设置不同字号,我们响应式地重新定义了整个网页的em和rem。
也就是说,即使不直接修改面板的样式,它也是响应式的。
在小屏上,比如智能手机上,字体会较小(12px),内边距和圆角也相应较小。
在大于800px和1200px的大屏上,组件会相应地分别放大到14px和16px的字号。缩放浏览器窗口可以看到这些变化

如果你足够严格,整个网页的样式都像这样使用相对单位定义,那么网页就会根据视口大小整体缩放。这是响应式策略中很重要的一部分。
靠近样式表顶部的两个媒体查询可以极大减少后续CSS代码中媒体查询的数量。如果用像素的话,就没有这么容易。

同样,如果老板或者客户觉得网页的字体太大或者太小,只需要改一行代码就能改变整体的字号,进而不费吹灰之力影响整个网页。

缩放单个组件

有时,需要让同一个组件在页面的某些部分显示不同的大小,你可以用em来单独缩放一个组件。
拿之前的面板举例。首先给面板加上一个large类:<div class="panel large">

下图展示了普通面板和大面板的区别。效果类似于响应式面板,但是同一个页面可以同时存在两种大小。

同一个页面里的普通面板和大面板

我们仍然使用相对单位,但是需要改变它相对的对象。
首先,给每个面板添加父元素声明 font-size: 1rem,这样无论面板位于页面何处,都有一个可预测的字号。

其次,改用em而不是用rem,重新定义标题的字号,使其相对于刚刚在 1rem 时创建的父元素的字号。

这次修改并不会影响面板的样式,但是它为创建更大的面板做好了准备:只需要加一行CSS代码,即覆盖父元素的1rem。
因为组件内所有的大小都是相对于父元素的字号,所以覆盖后,整个面板的大小都会改变。

现在对普通面板使用 class="panel",对大面板使用 class="panel large"
同理,可以设置一个更小的字号来定义一个小面板。如果面板是一个更复杂的组件,有多个字号和内边距,仍然只需要一个声明就能缩放它,只要内部的样式都使用em定义即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<!doctype html>
<head>
  <style>
    :root {
      font-size: 0.75em;
    }
    @media (min-width: 800px) {
      :root {
        font-size: 0.875em;
      }
    }
    @media (min-width: 1200px) {
      :root {
        font-size: 1em;
      }
    }

    body {
      font-family: Helvetica, Arial, sans-serif;
    }

    .panel {
      font-size: 1rem;
      padding: 1em;
      border: 1px solid #999;
      border-radius: 0.5em;
    }

    .panel > h2 {
      margin-top: 0;
      font-size: 0.8em;
      font-weight: bold;
      text-transform: uppercase;
    }

    .panel.large {
      font-size: 1.2em;
    }
  </style>
</head>

<body>
  <div class="panel">
    <h2>Single-origin</h2>
    <div class="body">
      We have built partnerships with small farms around the world to
      hand-select beans at the peak of season. We then carefully roast
      in <a href="/batch-size.html">small batches</a> to maximize their
      potential.
    </div>
  </div>
  <div class="panel large">
    <h2>Single-origin</h2>
    <div class="body">
      We have built partnerships with small farms around the world to
      hand-select beans at the peak of season. We then carefully roast
      in <a href="/batch-size.html">small batches</a> to maximize their
      potential.
    </div>
  </div>
</body>

视口的相对单位

前面介绍的 em 和 rem 都是相对于 font-size 定义的,但 CSS 里不止有这一种相对单位。
还有相对于浏览器视口定义长度的视口的相对单位

视口 —— 浏览器窗口里网页可见部分的边框区域。它不包括浏览器的地址栏、工具栏、状态栏。

如果你不熟悉视口的相对单位,请先看下面的简单介绍。

  • vh:视口高度的1/100。
  • vw:视口宽度的1/100。
  • vmin:视口宽、高中较小的一方的1/100
  • vmax:视口宽、高中较大的一方的1/100

比如,50vw 等于视口宽度的一半,25vh 等于视口高度的 25%
vmin取决于宽和高中较小的一方,这可以保证元素在屏幕方向变化时适应屏幕。
在横屏时,vmin取决于高度;在竖屏时,则取决于宽度。

下图展示了一个正方形元素在不同屏幕尺寸的视口中的样子。
它的宽度和高度都是 90vmin,等于宽高的较小边的 90%,即横屏高度的 90%,或者竖屏宽度的 90%

当一个元素的宽和高为90vmin时,不管视口的大小或者方向是什么,总会显示成一个稍小于视口的正方形:

视口示例1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!doctype html>
<head>
  <style>
    .square {
      width: 90vmin;
      height: 90vmin;
      background-color: #369;
    }
  </style>
</head>

<body>
  <div class="square"></div>
</body>

视口相对长度非常适合展示一个填满屏幕的大图。
我们可以将图片放在一个很长的容器里,然后设置图片的高度为100vh,让它等于视口的高度。

提示:
相对视口的单位对大部分浏览器而言是较新的特性,因此当你将它跟其他样式结合使用时,会有一些奇怪的bug。
可在 Can I Use 网站中检索 Viewport units: vw, vh, vmin, vmax 中的 “KnownIssues”。

CSS3

有些单位类型在CSS早期版本中没有(尤其是rem和视口的相对单位),它们是在CSS发展过程中加进来的,也就是通常所说的CSS3。

20世纪90年代末到21世纪初,在完成CSS规范的初始工作之后的很长一段时间,CSS几乎没有什么大的改变。
1998年5月,W3C(万维网联盟)发布了CSS2规范。紧接着开始制定2.1版本,对CSS2的问题和bug进行修正。
CSS2.1的制定工作持续了许多年,仍然没有增加重大的新特性,直到2011年4月,才作为提案推荐标准(Proposed Recommendation)发布。
此时,浏览器已经实现了CSS2.1的大部分特性,并且还以CSS3的名义增加了更多特性。

“3”是一个非正式的版本号,其实并没有CSS3规范,而是CSS规范分成了单独的模块,每个模块单独管理版本。
如今背景和边框的规范脱离了盒模型模块以及层叠和继承模块。这样W3C就能够制定CSS某个领域的新版本,而不需要更新其他没变的领域。
很多规范仍然停留在版本3(现在称作level 3),但是有一些规范已经处于level 4,比如选择器规范。
还有一些规范处于level 1,比如Flexbox。

随着这些变化的出现,我们发现从2009年到2013年,新特性呈现了爆炸式发展。
在此期间值得关注的新特性包括rem、视口的相对单位,以及新的选择器、媒体查询、Web字体、圆角边框、动画、过渡、变形、指定颜色的不同方式等。
现在每年还在不断地涌现新特性。

这也意味着我们不再只针对一个特定版本的CSS开发了。
CSS现在是一个活的标准(living standard)。每个浏览器在持续地增加对新特性的支持。
开发人员使用这些新特性,并且适应了这些变化。未来不会有CSS4了,除非人们拿它作为一个更通用的市场术语。

使用 vw 定义字号

相对视口单位有一个不起眼的用途,就是设置字号,但我发现它比用vh和vw设置元素的宽和高还要实用。

如果给一个元素加上 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两种单位。删除之前样式表的基础字号(以及相关的媒体查询),换成如下代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!doctype html>
<head>
  <style>
    :root {
      font-size: calc(0.5em + 1vw);
    }

    body {
      font-family: Helvetica, Arial, sans-serif;
    }

    .panel {
      font-size: 1rem;
      padding: 1em;
      border: 1px solid #999;
      border-radius: 0.5em;
    }

    .panel > h2 {
      margin-top: 0;
      font-size: 0.8em;
      font-weight: bold;
      text-transform: uppercase;
    }

    .panel.large {
      font-size: 1.2em;
    }
  </style>
</head>

<body>
  <div class="panel">
    <h2>Single-origin</h2>
    <div class="body">
      We have built partnerships with small farms around the world to
      hand-select beans at the peak of season. We then carefully roast
      in <a href="/batch-size.html">small batches</a> to maximize their
      potential.
    </div>
  </div>
</body>

现在打开网页,慢慢缩放浏览器,字体会平滑地缩放。
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 均相等。

警告
一个无单位的0只能用于长度值和百分比,比如内边距、边框和宽度等,而不能用于角度值,比如度,或者时间相关的值,比如秒。

line-height 属性比较特殊,它的值既可以有单位也可以无单位。
通常我们应该使用无单位的数值,因为它们继承的方式不一样。
我们在网页中加上一些文字,看看无单位的行高会如何影响样式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!doctype html>
<head>
  <style>
    body {
      /* 后代元素继承了无单位的值 */
      line-height: 1.2;
    }

    .about-us {
      font-size: 2em;
    }
  </style>
</head>

<body>
  <p class="about-us">
    We have built partnerships with small farms around the world to
    hand-select beans at the peak of season. We then carefully roast in
    small batches to maximize their potential.
  </p>
</body>

为每个后代元素计算无单位的行高

这个段落继承了行高1.2。因为段落字号是32px(2em × 16px,浏览器默认字号),所以此时行高的计算值为 38.4px(32px × 1.2)。每行文字之间都会有一个合理的间距。

如果用有单位的值定义行高,可能会产生意想不到的结果,如图所示。每行文字会重叠。

1
2
3
4
5
6
7
body {
    line-height: 1.2em;
}

.about-us {
    font-size: 2em;
}

继承line-height导致行重叠

这些结果源于继承的一个怪异特性:当一个元素的值定义为长度(px、em、rem,等等)时,子元素会继承它的计算值。
当使用em等单位定义行高时,它们的值是计算值,传递到了任何继承子元素上。
如果子元素有不同的字号,并且继承了line-height属性,就会造成意想不到的结果,比如文字重叠。

长度 —— 一种用于测量距离的CSS值的正式称谓。
它由一个数值和一个单位组成,比如 5px。长度有两种类型:绝对长度和相对长度。百分比类似于长度,但是严格来讲,它不是长度。

使用无单位的数值时,继承的是声明值,即在每个继承子元素上会重新算它的计算值。这样得到的结果几乎总是我们想要的。
我们可以用一个无单位的数值给body设置行高,之后就不用修改了,除非有些地方想要不一样的行高。

自定义属性(即 CSS 变量)

2015年,一个期盼已久的CSS规范作为候选推荐标准问世了,叫作层叠变量的自定义属性(Custom Properties for Cascading Variables)。
这个规范给CSS引进了变量的概念,开启了一种全新的基于上下文的动态样式。
你可以声明一个变量,为它赋一个值,然后在样式表的其他地方引用这个值。这样不仅能减少样式表中的重复,而且还有其他好处。

要了解更新各大主流浏览器的支持的情况,请在Can I Use网站中检索“CSS Variables”。

说明
如果刚好用了内置变量功能的CSS预处理器,比如Sass或者Less,你可能就不太想用CSS变量了。
千万别这样。新规范里的CSS变量有本质上的区别,它比任何一款预处理器的变量功能都多。
因此我倾向于称其为“自定义属性”,而不是变量,以强调它跟预处理器变量的区别。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!doctype html>
<head>
  <style>
    :root {
      --main-font: Helvetica, Arial, sans-serif;
    }

    p {
      font-family: var(--main-font);
    }
  </style>
</head>

<body>
  <p>
    We have built partnerships with small farms around the world to
    hand-select beans at the peak of season. We then carefully roast
    in small batches to maximize their
    potential.
  </p>
</body>

上面的代码定义了一个名叫 --main-font 的变量。将其值设置为一些常见的sans-serif字体。
变量名前面必须有两个连字符(--),用来跟CSS属性区分,剩下的部分可以随意命名。

变量必须在一个声明块内声明。这里使用了 :root 选择器,因此该变量可以在整个网页使用

变量声明本身什么也没做,我们使用时才能看到效果。

调用函数var()就能使用该变量。利用该函数引用前面定义的变量--main-font

在样式表某处为自定义属性定义一个值,作为“单一数据源”,然后在其他地方复用它。
这种方式特别适合反复出现的值,比如颜色值。

在样式表中可以多次使用这个变量,当你想要改变这个颜色值时,只需要在一个地方修改即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!doctype html>
<head>
  <style>
    :root {
      --main-font: Helvetica, Arial, sans-serif;
      --brand-color: #369;
    }

    p {
      font-family: var(--main-font);
      color: var(--brand-color);
    }
  </style>
</head>

<body>
  <p>
    We have built partnerships with small farms around the world to
    hand-select beans at the peak of season. We then carefully roast
    in small batches to maximize their
    potential.
  </p>
</body>

var()函数接受第二个参数,它指定了备用值。如果第一个参数指定的变量未定义,那么就会使用第二个值。

1
2
3
4
5
6
7
8
9
:root {
    --main-font: Helvetica, Arial, sans-serif;
    --brand-color: #369;
}

p {
    font-family: var(--main-font, sans-serif);
    color: var(--brand-color, blue);
}

说明
如果var()函数算出来的是一个非法值,对应的属性就会设置为其初始值。
比如,如果在 padding: var(--brand-color) 中的变量算出来是一个颜色,它就是一个非法的内边距值。
这种情况下,内边距会设置为0。

动态改变自定义属性

在前面的示例中,自定义属性只不过为减少重复代码提供了一种便捷方式,
但是它真正的意义在于,自定义属性的声明能够层叠和继承:可以在多个选择器中定义相同的变量,这个变量在网页的不同地方有不同的值。

例如,可以定义一个变量为黑色,然后在某个容器中重新将其定义为白色。
那么基于该变量的任何样式,在容器外部会动态解析为黑色,在容器内部会动态解析为白色。接下来用这种特性来实现如下图所示的效果。

自定义属性基于当前变量值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<!doctype html>
<head>
  <style>
    :root {
      font-size: calc(0.5em + 1vw);
      --main-bg: #fff;
      --main-color: #000;
    }

    body {
      font-family: Helvetica, Arial, sans-serif;
    }

    .dark {
      margin-top: 2em;
      padding: 1em;
      background-color: #999;
      --main-bg: #333;
      --main-color: #fff;
    }

    .panel {
      font-size: 1rem;
      padding: 1em;
      border: 1px solid #999;
      border-radius: 0.5em;
      background-color: var(--main-bg);
      color: var(--main-color);
    }

    .panel > h2 {
      margin-top: 0;
      font-size: 0.8em;
      font-weight: bold;
      text-transform: uppercase;
    }

    .panel.large {
      font-size: 1.2em;
    }
  </style>
</head>

<body>
  <div class="panel">
    <h2>Single-origin</h2>
    <div class="body">
      We have built partnerships with small farms
      around the world to hand-select beans at the
      peak of season. We then carefully roast in
      small batches to maximize their potential.
    </div>
  </div>

  <aside class="dark">
    <div class="panel">
      <h2>Single-origin</h2>
      <div class="body">
        We have built partnerships with small farms
        around the world to hand-select beans at the
        peak of season. We then carefully roast in
        small batches to maximize their potential.
      </div>
    </div>
  </aside>
</body>

可以看到第二个面板有深色背景和白色文字。
这是因为面板使用了这些变量,它们会解析成深色容器内定义的值,而不是根元素内定义的值。
注意,这里并没有重新定义面板样式,或者给面板加上额外的类。

在本例中,总共定义了自定义属性两次:第一次在根元素上(--main-color为黑色),第二次在深色容器上(--main-color 为白色)。
自定义属性就像作用域变量一样,因为它的值会被后代元素继承。
在深色容器中,--main-color为白色,在页面其他地方,则是黑色。

使用 JavaScript 改变自定义属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<!doctype html>
<head>
  <style>
    :root {
      font-size: calc(0.5em + 1vw);
      --main-bg: #fff;
      --main-color: #000;
    }

    body {
      font-family: Helvetica, Arial, sans-serif;
    }

    .dark {
      margin-top: 2em;
      padding: 1em;
      background-color: #999;
      --main-bg: #333;
      --main-color: #fff;
    }

    .panel {
      font-size: 1rem;
      padding: 1em;
      border: 1px solid #999;
      border-radius: 0.5em;
      background-color: var(--main-bg);
      color: var(--main-color);
    }

    .panel > h2 {
      margin-top: 0;
      font-size: 0.8em;
      font-weight: bold;
      text-transform: uppercase;
    }

    .panel.large {
      font-size: 1.2em;
    }
  </style>
</head>

<body>
  <div class="panel">
    <h2>Single-origin</h2>
    <div class="body">
      We have built partnerships with small farms
      around the world to hand-select beans at the
      peak of season. We then carefully roast in
      small batches to maximize their potential.
    </div>
  </div>

  <aside class="dark">
    <div class="panel">
      <h2>Single-origin</h2>
      <div class="body">
        We have built partnerships with small farms
        around the world to hand-select beans at the
        peak of season. We then carefully roast in
        small batches to maximize their potential.
      </div>
    </div>
  </aside>

  <script type="text/javascript">
  let rootElement = document.documentElement;
let styles = getComputedStyle(rootElement);
let mainColor = styles.getPropertyValue('--main-bg');
console.log(String(mainColor).trim());

rootElement = document.documentElement;
rootElement.style.setProperty('--main-bg', '#cdf');
</script>
</body>

如果运行以上脚本,所有继承了--main-bg属性的元素都会更新,使用新的值。
在网页中,第一个面板的背景色会改为浅蓝色。第二个面板保持不变,因为它依然继承了深色容器里的属性。

利用这种技术,就可以用JavaScript实时切换网站主题,或者在网页中突出显示某些元素,或者实时改变任意多个元素。
只需要几行 JavaScript 代码,就可以进行更改,从而影响网页上的大量元素。

探索自定义属性

自定义属性是CSS中一个全新的领域,开发人员刚刚开始探索。因为浏览器支持有限,所以还没有出现“典型”的用法。
我相信假以时日,会出现各种最佳实践和新的用法。这需要你持续关注。继续使用自定义属性,看看能用它做出什么效果。

值得注意的是,在不支持自定义属性的浏览器上,任何使用var()的声明都会被忽略。请尽量为这些浏览器提供回退方案。

1
2
color: black;
color: var(--main-color);

然而这种做法不是万能的,比如当用到自定义属性的动态特性时,就很难有备用方案。关注Can I Use网站,查看最新的浏览器支持情况。

总结

  • 拥抱相对单位,让网页的结构决定样式的含义。
  • 建议用rem设置字号,但是有选择地用em实现网页组件的简单缩放。
  • 不用媒体查询也能让整个网页响应式缩放。
  • 使用无单位的值设置行高。
  • 请开始熟悉CSS的一个新特性:自定义属性。