第 2 章 相对单位

第 2 章 相对单位

本章概要

  • 相对单位的广泛用途
  • 使用em和rem
  • 使用视口的相对单位
  • 介绍CSS变量

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

开发人员,即便是经验丰富的CSS开发人员,通常也不愿意使用相对单位,包括经常提到的em。em值变化的方式使其难以预测,不如像素简单明了。本章将揭开相对单位的神秘面纱。首先我会解释相对单位给CSS带来的独特价值,并帮助你理解它们。我会解释它们的工作原理,然后展示如何控制其看似不可预测的性质。相对单位可以为我们所用,用得恰当的话,它们会让代码更简洁、更灵活,也更简单。

2.1 相对值的好处

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

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

11英寸约合2.54厘米。——编者注

2.1.1 那些年追求的像素级完美

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

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

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

2.1.2 像素级完美的时代终结了

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

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

 响应式——在CSS中指的是样式能够根据浏览器窗口的大小有不同的“响应”。这要求有意地考虑任何尺寸的手机、平板设备,或者桌面屏幕。第8章会详细介绍响应式设计,但本章会先普及一些重要的基础知识。

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通常等于一个物理英寸的大小。

2.2 em和rem

em是最常见的相对长度单位,适合基于特定的字号进行排版。在CSS中,1em等于当前元素的字号,其准确值取决于作用的元素。图2-1是一个内边距为1em的div元素。

{%}

图2-1 内边距为1em的元素(虚线用于展示内边距)

它的代码如代码清单2-1所示。规则集指定了字号为16px,也就是元素局部定义的1em。然后使用em指定了元素的内边距。将代码清单2-1加入一个新的样式表,在<div class="padded">中写一些文字,在浏览器中看看会是什么效果。

代码清单2-1 用em单位设置内边距

.padded {
  font-size: 16px;         
  padding: 1em;    ←---- 设置四个内边距为font-size
}

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

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

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

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

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

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

如代码清单2-2所示,在HTML中创建两个盒子。给元素分别添加box-smallbox-large类名,作为大小修饰符。

代码清单2-2 给不同的元素加上em(HTML)

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

现在将代码清单2-3加到样式表中。这段代码用em定义了一个盒子,同时定义了一个small和一个large的修饰符,分别指定不同的字号。

代码清单2-3 将em应用于不同的元素(CSS)

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

.box-small {
  font-size: 12px;     (以下5行)不同的字号,可以决定元素的em值
}                           

.box-large {                
  font-size: 18px;       
}

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

2.2.1 使用em定义字号

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

举个简单的例子。如图2-3所示,有两段文字,分别有不同的字号。可以像代码清单2-4那样定义元素,然后使用em定义字号。

图2-3 使用em定义两种不同的字号

按照代码清单2-4修改网页。第一行文字在<body>标签中,因此它会按照body的字号来渲染。第二段的slogan继承了这个字号。

代码清单2-4 使用相对font-size的标记

<body>
  We love coffee                             
  <p class="slogan">We love coffee</p>   ←---- slogan继承了<body>的字号
</body>

代码清单2-5指定了元素的字号。简单起见,这里用像素单位。接下来使用em来放大slogan的字号。

代码清单2-5 使用em定义font-size

body {
  font-size: 16px;
}

.slogan {       (以下3行)计算结果为元素继承的字号的1.2倍
  font-size: 1.2em;    
}

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

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

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

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

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

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

    图2-4 字号为1.2em和内边距为1.2em的元素

    这是因为该段落从body继承了16px的字号,最终字号的计算值为19.2px。因此19.2px是em的局部值,用于计算内边距。按照代码清单2-6更新测试页面的样式表。

    代码清单2-6 使用em定义font-sizepadding

    body {
      font-size: 16px;
    }
    
    .slogan {                   
      font-size: 1.2em;      ←---- 计算值为19.2px       
      padding: 1.2em;        ←---- 计算值为23.04px
      background-color: #ccc;
    }

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

  2. 字体缩小的问题

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

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

    图2-5 嵌套列表的文字缩小了

    当列表多级嵌套并且给每一级使用em定义字号时,就会发生文字缩小的现象。代码清单2-7和代码清单2-8的例子里,设置无序列表的字号为0.8em。选择器选中了网页上每个<ul>元素,因此当这些列表从其他列表继承字号时,em就会逐渐缩小字号。

    代码清单2-7 使用em指定列表的字号

    body {
      font-size: 16px;
    }
    
    ul {
      font-size: .8em;
    }

    代码清单2-8 嵌套列表

    <ul>
      <li>Top level
        <ul>           (以下2行)这个列表嵌套在第一个列表中,继承它的字号
          <li>Second level     
            <ul>          (以下2行)这个嵌套在上一个列表中,继承第二个列表的字号
              <li>Third level      
                <ul>       (以下2行)以此类推
                  <li>Fourth level    
                    <ul>
                      <li>Fifth level</li>
                    </ul>
                  </li>
                </ul>
              </li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>

    每个列表元素的字号等于0.8乘以其父元素的字号。算出来第一级列表的字号为12.8px,第二级缩小到10.24px(12.8px × 0.8),第三级缩小到8.192px,以此类推。同理,如果指定一个大于1em的字号,文字会逐渐增大。我们想要的是指定顶部的字号,然后保持子级的字号一致,如图2-6所示。

    图2-6 文字大小正确的嵌套列表

    实现这种效果的代码如代码清单2-9所示。它设置第一级列表的字体为0.8em(跟代码清单2-7一致)。代码清单2-9里的第二个选择器选中了嵌套在某个无序列表中的所有无序列表,也就是除了顶级列表以外的其他列表。嵌套列表的字号等于其父级的字号,如图2-6所示。

    代码清单2-9 纠正文字缩小的问题

    ul {
      font-size: .8em;
    }
    
    ul ul {         (以下3行)嵌套的列表应当跟其父级的字号一致
      font-size: 1em;     
    }

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

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

2.2.2 使用rem设置字号

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

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

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

代码清单2-10 使用rem指定字号

:root {                       ←---- :root伪类等价于类型选择器html
  font-size: 1em;       ←---- 使用浏览器的默认字号(16px)
}                          

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设置其他大部分属性。

2.3 停止像素思维

过去几年有一个常见的模式,更准确地说是反模式,就是将网页根元素的字号设置为0.625em或者62.5%(如代码清单2-11所示)。

代码清单2-11 反模式:全局重置font-size为10px

html {
  font-size: .625em;
}

我不推荐这样写。代码清单2-11将浏览器的默认字号16px缩小为10px。这的确能简化计算:如果设计师希望字号为14px,那么只需要默默在心里除以10,写上1.4rem就可以了,而且还使用了相对单位。

一开始,这会很方便,但是这样有两个缺点。第一,我们被迫写很多重复的代码。10px对大部分文字来说太小了,所以需要覆盖它,最后就得给段落设置1.4rem,给侧边栏设置1.4rem,给导航链接设置1.4rem,等等。这样一来,代码容易出错的地方更多;当需要修改代码时,要改动的地方更多;样式表的体积也更大。

第二,这种做法的本质还是像素思维。虽然在代码里写的是1.4rem,但是在心里仍然想着“14像素”。在响应式网页中,需要习惯“模糊”值。1.2em到底是多少像素并不重要,重点是它比继承的字号要稍微大一点。如果在屏幕上的效果不理想,就调整它的值,反复试验。这种方式同样适用于像素值。(在第13章中,我们将进一步研究具体规则来改进这种方法。)

使用em时,很容易陷入沉思:到底计算出来的像素值是多少,尤其是用em定义字号时。你会不停地做乘法和除法来计算em的值,直到抓狂。相反,我建议先适应使用em。如果已经习惯了像素,那么使用em可能需要反复练习,但这一切是值得的。

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

将大小转换成rem需要计算,记得随手带一个计算器。(我会在Mac上按Command-Space键,在Spotlight里输入算式。)给根元素设置了字号后,就定义了一个rem。在这之后,应该只在少数特殊情况下使用像素,而不能经常使用。

我在本章会继续提及像素,方便解释相对单位的行为,以及帮你熟悉em的计算。在本章之后,我将主要使用相对单位讨论字号。

2.3.1 设置一个合理的默认字号

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

将代码清单2-12加入到一个新的样式表中,作为基础样式。代码清单2-12设置了根元素(<html>)的默认字体。

代码清单2-12 设置真正的默认字号

:root {        ←----  使用HTML选择器
  font-size: 0.875em;     ←---- 14/16(理想的px/继承的px)=0.875
}

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

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

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

面板的HTML标记如代码清单2-13所示,将其添加到网页中。

代码清单2-13 面板的HTML标记

<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">small batches</a> to maximize their
    potential.
  </div>
</div>

代码清单2-14是面板的样式代码。这里用em设置内边距和圆角,用rem设置标题的字号,用px设置边框。将以下代码添加到你的样式表中。

代码清单2-14 使用相对单位创建的面板

.panel {
  padding: 1em;        (以下2行)用em设置内边距和圆角
  border-radius: 0.5em;    
  border: 1px solid #999;    ←---- 用1px设置一条细边
}

.panel > h2 {                
  margin-top: 0;         ←---- 将面板顶部的多余空间移除,第3章会对此详细解释
  font-size: 0.8rem;      (以下3行)用rem设置标题的字号
  font-weight: bold;             
  text-transform: uppercase;     
}

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

第二个选择器里的>是一个直接后代组合器。它选中了.panel元素的一个h2子元素。有关选择器和组合器的完整参考资料,参见附录A。

在代码清单2-13中,给面板主体添加panel-body类只是为了明确含义,在CSS中并未用到。因为这个元素已经继承了根元素的字号,所以它看起来就是理想的样子,不需要覆盖。

2.3.2 构造响应式面板

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

 媒体查询,即@media规则,可以指定某种屏幕尺寸或者媒体类型(比如,打印机或者屏幕)下的样式。这是响应式设计的关键部分。这里以代码清单2-15为例进行说明,第8章将更深入地介绍媒体查询。

{%}

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

按照代码清单2-15修改你的样式表。

代码清单2-15 响应式的根元素font-size

:root {          (以下3行)作用到所有的屏幕,但是在大屏上会被覆盖
  font-size: 0.75em;   
}                      

@media (min-width: 800px) {   (以下5行)仅作用到宽度800px及其以上的屏幕,覆盖之前的值
  :root {                     
    font-size: 0.875em;       
  }                           
}                             

@media (min-width: 1200px) {  (以下5行)仅作用到宽度1200px及其以上的屏幕,覆盖前面两个值
  :root {                      
    font-size: 1em;           
  }                            
}

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

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

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

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

2.3.3 缩放单个组件

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

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

图2-9 同一个页面里的普通面板和大面板

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

其次,改用em而不是用rem,重新定义标题的字号,使其相对于刚刚在1rem时创建的父元素的字号。用代码清单2-16所示的代码,更新你的样式表。

代码清单2-16 创建一个大面板

.panel {
  font-size: 1rem;          ←--- 给组件设置一个可预测的字号
  padding: 1em;                 
  border: 1px solid #999;
  border-radius: 0.5em;
}

.panel > h2 {
  margin-top: 0;        (以下3行)用em定义其他字号,使其相对于父元素的字号
  font-size: 0.8em;           
  font-weight: bold;             
  text-transform: uppercase;
}

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

代码清单2-17 用一个CSS声明放大整个面板

.panel.large {         ←---- 复合选择器选中同时拥有panel和large类的元素
  font-size: 1.2rem;      
}

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

2.4 视口的相对单位

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

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

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

  • vh:视口高度的1/100。
  • vw:视口宽度的1/100。
  • vmin:视口宽、高中较小的一方的1/100(IE9中叫vm,而不是vmin)。
  • vmax:视口宽、高中较大的一方的1/100(本书写作时IE和Edge均不支持vmax)2

2翻译本书时Edge已支持vmax。——译者注

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

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

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

代码清单2-18是该元素的CSS样式。它生成了一个大正方形,不管如何缩放浏览器,它都能在视口中显示。可以在网页里加上<div class="square">来看效果。

代码清单2-18 用vmin定义正方形元素的大小

.square {
  width: 90vmin;
  height: 90vmin;
  background-color: #369;
}

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

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

 

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了,除非人们拿它作为一个更通用的市场术语。虽然本书覆盖了CSS3的特性,但是我尽可能不这么称呼它们,因为对于Web来说,它们都属于CSS。

2.4.1 使用vw定义字号

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

如果给一个元素加上font-size: 2vw会发生什么?在一个1200px的桌面显示器上,计算值为24px(1200的2%)。在一个768px宽的平板上,计算值约为15px(768的2%)。这样做的好处在于元素能够在这两种大小之间平滑地过渡,这意味着不会在某个断点突然改变。当视口大小改变时,元素会逐渐过渡。

不幸的是,24px在大屏上来说太大了。更糟糕的是,在iPhone 6上会缩小到只有7.5px。如果能够保留这种缩放的能力,但是让极端情况缓和一些就更棒了。CSS的calc()函数可以提供帮助。

2.4.2 使用calc()定义字号

calc()函数内可以对两个及其以上的值进行基本运算。当要结合不同单位的值时,calc()特别实用。它支持的运算包括:加(+)、减(-)、乘(×)、除(÷)。加号和减号两边必须有空白,因此我建议大家养成在每个操作符前后都加上一个空格的习惯,比如calc(1em + 10px)

代码清单2-19用calc()结合了em和vw两种单位。删除之前样式表的基础字号(以及相关的媒体查询),换成如下代码。

代码清单2-19 用calc()结合em和vh两种单位定义font-size

:root {
  font-size: calc(0.5em + 1vw);
}

现在打开网页,慢慢缩放浏览器,字体会平滑地缩放。0.5em保证了最小字号,1vw则确保了字体会随着视口缩放。这段代码保证基础字号从iPhone 6里的11.75px一直过渡到1200px的浏览器窗口里的20px。可以按照自己的喜好调整这个值。

我们不用媒体查询就实现了大部分的响应式策略。省掉三四个硬编码的断点,网页上的内容也能根据视口流畅地缩放。

2.5 无单位的数值和行高

有些属性允许无单位的值(即一个不指定单位的数)。支持这种值的属性包括line-heightz-indexfont-weight(700等于bold,400等于normal,等等)。任何长度单位(如px、em、rem)都可以用无单位的值0,因为这些情况下单位不影响计算值,即0px、0%、0em均相等。

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

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

代码清单2-20 继承line-height的标记

<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>

接下来给body元素指定一个行高,允许它被网页上其他元素继承。不管在网页设置了什么字号,这种方式都会按照预期显示(如图2-11所示)。

图2-11 为每个后代元素重新计算无单位的行高

将代码清单2-21添加到样式表中。这个段落继承了行高1.2。因为段落字号是32px(2em × 16px,浏览器默认字号 ),所以此时行高的计算值为38.4px(32px×1.2)。每行文字之间都会有一个合理的间距。

代码清单2-21 用无单位的数值定义的行高

body {
  line-height: 1.2;   ←---- 后代元素继承了无单位的值
}                        

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

如果用有单位的值定义行高,可能会产生意想不到的结果,如图2-12所示。每行文字会重叠。对应的CSS如代码清单2-22所示。

图2-12 继承line-height,导致行重叠

代码清单2-22 用有单位的值定义行高,产生了意想不到的结果

body {            (以下3行)后代元素继承了计算值(19.2px)
  line-height: 1.2em;  
}                          

.about-us {
  font-size: 2em;   ←---- 计算值为32px
}

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

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

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

2.6 自定义属性(即CSS变量)

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

写作本书时,除了IE,自定义属性已经得到各大主流浏览器的支持。要了解更新更全的情况,请在Can I Use网站中检索“CSS Variables”。

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

要定义一个自定义属性,只需要像其他CSS属性那样声明即可,如代码清单2-23所示。创建一个新的网页和样式表,将代码清单2-23添加到样式表中。

代码清单2-23 定义一个自定义属性

:root {
  --main-font: Helvetica, Arial, sans-serif;
}

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

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

变量声明本身什么也没做,我们使用时才能看到效果。将这个变量用到一个段落上,就会产生如图2-13所示的结果。

图2-13 该段落使用了变量里定义的sans-serif字体

调用函数var()就能使用该变量。利用该函数引用前面定义的变量--main-font。将代码清单2-24里的规则集添加到你的样式表中。

代码清单2-24 使用自定义属性

:root {
  --main-font: Helvetica, Arial, sans-serif;
}

p {                (以下3行)将段落的字体设置为Helvetica、Arial、sans-serif
  font-family: var(--main-font);   
}

在样式表某处为自定义属性定义一个值,作为“单一数据源”,然后在其他地方复用它。这种方式特别适合反复出现的值,比如颜色值。代码清单2-25添加了一个叫brand-color的自定义属性。在样式表中可以多次使用这个变量,当你想要改变这个颜色值时,只需要在一个地方修改即可。

代码清单2-25 使用自定义属性定义颜色

:root {
  --main-font: Helvetica, Arial, sans-serif;     
  --brand-color: #369;                        ←---- 定义一个蓝色的brand-color变量
}

p {
  font-family: var(--main-font);
  color: var(--brand-color);
}

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

代码清单2-26在两个不同的声明中都指定了备用值。在第一个声明里,因为--main-font被定义为Helvetica, Arial, sans-serif,所以使用了这个变量的值。在第二个声明里,因为--secondary-color是一个未定义的变量,所以使用了备用值blue

代码清单2-26 提供备用值

:root {
  --main-font: Helvetica, Arial, sans-serif;
  --brand-color: #369;
}

p {                                              
  font-family: var(--main-font, sans-serif);  ←---- 指定备用值为sans-serif       
  color: var(--secondary-color, blue);        ←---- secondary-color变量没有定义,因此会使用备用值blue
}

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

2.6.1 动态改变自定义属性

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

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

图2-14 自定义属性基于当前变量值,产生了两种不同颜色的面板

这个面板跟之前的面板(如图2-7所示)类似。它的HTML标记如代码清单2-27所示。代码里的面板有两个实例:一个面板在body里,还有一个面板在深色的区域中。按照代码清单2-27更新HTML。

代码清单2-27 同一个网页中,不同环境下的两个面板

<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 careful roast in
      small batches to maximize their potential.
    </div>
  </div>

  <aside class="dark">    (以下2行)深色容器内的另一个面板
    <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 careful roast in
        small batches to maximize their potential.
      </div>
    </div>
  </aside>
</body>

接下来,用变量定义文字和背景颜色,进而重新定义这个面板。将代码清单2-28加入你的样式表。这会将背景色设置为白色,将文字设置为黑色。在实现深色面板之前,我先解释一下它的工作原理。

代码清单2-28 使用变量定义面板颜色

:root {
  --main-bg: #fff;      (以下2行)分别将背景色和文字颜色变量定义为白色和黑色
  --main-color: #000;   
}

.panel {
  font-size: 1rem;
  padding: 1em;
  border: 1px solid #999;
  border-radius: 0.5em;
  background-color: var(--main-bg);    (以下2行)在面板样式中使用变量
  color: var(--main-color);             
}

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

首先还是在:root选择器的规则集中定义变量。这很重要,如此一来这些值就可以提供给根元素(整个网页)下的任何元素。当根元素的后代元素使用这个变量时,就会解析这里的值。

我们有两个面板,它们看起来一样。接下来在另一个选择器中重新定义这两个变量。代码清单2-29定义了深色容器的样式,为该容器设置了深灰色背景,还有一些内边距和外边距,同时也重新定义了两个变量。将代码清单2-29添加到样式表中。

代码清单2-29 深色容器的样式

.dark {
  margin-top: 2em;   ←---- 给深色容器和前面的面板之间加上外边距
  padding: 1em;         

  background-color: #999;    ←---- 给深色容器加上深灰色背景
  --main-bg: #333;      (以下2行)在容器内重定义--main-bg和--main-color变量
  --main-color: #fff;    
}

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

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

2.6.2 使用JavaScript改变自定义属性

还可以使用JavaScript在浏览器中实时访问和修改自定义属性。本书并不是介绍JavaScript的,所以只会简单介绍概念。需要你自己在JavaScript项目中实现剩下的功能。

代码清单2-30展示了如何访问一个元素上的属性。在网页中插入一个脚本,该脚本记录了根元素的--main-bg属性值。

代码清单2-30 访问JavaScript的自定义属性

<script type="text/javascript">
  var rootElement = document.documentElement;
  var styles = getComputedStyle(rootElement);    ←-- 获取一个元素的styles对象
  var mainColor = styles.getPropertyValue('--main-bg');   ←-- 获取styles对象的--main-bg值
  console.log(String(mainColor).trim());   ←---- 确保mainColor是一个字符串,并去掉前后空格;打印结果为“#fff”
</script>

因为你可以实时改变自定义属性的值,所以可以用JavaScript为--main-bg动态设置一个新值。如果将其设置为浅蓝色,效果会如图2-15所示。

图2-15 JavaScript可以通过改变--main-bg变量的值,设置面板的背景色

代码清单2-31给根元素上的--main-bg设置了一个新值。将这个代码清单放到<script>标签的末尾。

代码清单2-31 使用JavaScript设置一个自定义属性

var rootElement = document.documentElement;
rootElement.style.setProperty('--main-bg', '#cdf');  ←---- 将根元素上的--main-bg设置为浅蓝色

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

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

2.6.3 探索自定义属性

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

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

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

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

2.7 总结

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

目录