CSS 排版与间距

本章概要

  • 元素间距的微调
  • 用 Web 字体打造页面独特观感
  • 谷歌字体 API 的用法
  • 字体间距的调整(字距、行距与行高)
  • 影响 Web 字体性能的因素及优化策略
  • 可变字体(variable fonts)的用法

本章将继续构建上一章留下的示例页。目前已经定义好了颜色,页面布局也大致准备就绪了。剩下的工作还包括精修页面元素间的间距,以及改用 Web 字体来增强页面的视觉趣味性等。它们看似是两个不相关的主题,但在某些关键问题上是存在相互作用的。CSS 中含有大量控制布局、间距及字号的属性(properties),因此有必要将它们与某些通用的间距调整方案结合起来进行考察。

排版是与印刷出版一样历史悠久,也因此成为本书中唯一一个有着数百年发展历史的古老话题。本章不会过于详尽地阐述这部分内容,但会介绍一些要点,以及如何将其应用到现代 Web 开发中。

12.1 间距

Spacing

如果上一章您一直紧跟我的节奏在本地同步练习的话,此刻应该已经实现了示例页的颜色配置和大致的布局。示例页的 HTML 标记详见 代码清单 11.1(译注:位于上一章的 11.1.2 小节);而 CSS 代码则是在后续讲解中逐步完善的,此刻也应该与本章中的示例代码保持一致。如若不然,也可以从示例代码仓库中复制文件 listing-11.08.html(详见 https://github.com/CSSInDepth/css-in-depth-2/blob/main/ch11/listing-11.08.html)。

下面重点关注设计稿中标注的那些精确间距。根据设计师选用的不同工具,间距的微调往往是开发过程中一项较为枯燥的工作。虽然现代化的设计工具能有效缓解这个问题,但在后期审查页面时,可能还会与设计师反复沟通调整,明确一些需要修改的地方,或者跟视觉稿不一致的实现。

这部分工作主要涉及元素外边距的正确设置。人们往往是从最容易的地方着手,哪怕后续会面临进一步调整。这里要考虑的问题主要有两个:一是相对单位的引入与否;二是考虑行高对垂直间距的实际影响。

12.1.1 使用 em 还是 px

Using ems vs. px

考虑使用相对单位还是绝对单位,是非常重要的决定。因为设计师一般使用像素来标注距离,因此使用绝对单位会相对容易。但启用相对单位,无论是选 em 还是 rem,都可以带来很多好处。

我们来看一下导航菜单里标注的距离(如图 12.1 所示)。设计稿要求每个导航菜单项之间必须留有 10px 的间距,同时其底边与导航条的底边之间的间距也为 10px

图 12.1 每个导航菜单项之间及周围都需要空出 10px
图 12.1 每个导航菜单项之间及周围都需要空出 10 px

在第二章中,我们了解了使用相对单位的各种好处,因此有必要想清楚对哪些尺寸适合相对单位,而哪些适合用像素。是考虑快刀斩乱麻,将所有尺寸都转为 em 或者 rem?还是因地制宜,根据页面每个设计元素的具体情况来决定?如果考虑间距会随着用户设置的不同字号做同步缩放,那么就应该选用相对单位 emrem;否则使用像素单位 px 就完全足够了。基于这样的考虑,在实际开发中,我通常更倾向于对一些偏小的尺寸应用相对单位(em),特别是那些环绕在文字或按钮周围的尺寸;而稍大一些的容器间隔或内部间距受响应式设计的影响没那么敏感,因此保留像素单位 px 即可。

在我带您了解示例页中的间距设置时,我会指出选择某种方式的理由;但请注意,这些意见或观点难免会带有主观色彩,给出的解决方案也可能并非唯一正确的答案。

按照设计规范,导航栏中的间距尺寸需要在菜单项四周留出 10px 的距离(如图 12.1 所示)。由于它们算小尺寸元素,且文字周围还得留些内边距,因此考虑使用相对单位 em 来设置它们的间距。

鉴于基准字号为 16px,可以通过目标尺寸除以基准字号来算出 em 的值,即 10 / 16 = 0.625。因此该间距为 0.625em;再将其放入样式表中,相关样式代码如下列代码清单 12.1 所示。需要变更的地方都标有注释。

译注
为方便对照,这里直接给出示例页的相关 HTML 标记:

<nav class="nav-container">
  <div class="nav-container__inner">
    <!--<a class="home-link" href="/">Ink</a>-->
    <ul class="top-nav">
      <li><a href="/features">Features</a></li>
      <li><a href="/pricing">Pricing</a></li>
      <li><a href="/support">Support</a></li>
      <li class="top-nav__featured"><a href="/login">Login</a></li>
    </ul>
  </div>
</nav>

代码清单 12.1 使用内边距和外边距来设置导航菜单的间距

.nav-container {
  background-color: var(--medium-green);
}
.nav-container__inner {
  display: flex;
  justify-content: space-between;
  max-inline-size: 1080px;
  margin-inline: auto;
  padding: 0.625em 0; /* 给整个导航栏设置 10px 的上下内边距 */
}

/* ... */

.top-nav {
  display: flex;
  list-style-type: none;
  margin: unset; /* 移除浏览器默认的列表元素外边距样式 */
  gap: 0.625em; /* 各导航菜单项间添加 10px 的水平外边距*/
}

处理间距时,需要知道什么时候该用内边距,什么时候该用外边距。在本例中,容器 nav-container__inner 应该使用内边距来设置垂直间隔,以便对整个容器生效,让其中靠左显示的页面标题(即 a.home-link 元素)和 top-nav 列表自带上下间距;而导航菜单项之间的水平间距则用到了 Flexbox 布局中的间隙(gap),因为我只希望间距出现在各子项之间。此外,也可以通过设置外边距来达到相同效果 [1]

再来看看巨幅主图的底边和三个内容栏之间的垂直间隔。如图 12.2 所示,设计稿展示了这些间距的测量结果。可以看到,无论对于带背景图片的主图元素还是设置了背景色的三个内容栏元素,标出的间隔都仅对元素外围生效,因此需要使用外边距来处理这两个间隔。

图 12.2 主图下方(40px)及内容栏(25px)之间的页面外边距效果
图 12.2 主图下方(40 px)及内容栏(25 px)之间的页面外边距效果

在本例中,我认为这些间隔无需随字号大小而同步缩放,因此就保留了像素单位 px。具体设置详见代码清单 12.2。上一章其实已经设置了主图下方的外边距 40px,这里再写一遍以强调其用意。将下列示例代码中 tile-row 元素的间隙设置(即 gap 声明)也添加到本地样式表:

代码清单 12.2 为主图下方及内容栏之间设置外边距

.hero {
  background: url(collaboration.jpg) no-repeat;
  background-size: cover;
  margin-block-end: 40px; /* 确保主图下方有 40px 的间距 */
}

/* ... */

.tile-row {
  display: flex;
  gap: 25px; /* 各分栏之间保持 25px 的间距*/
}
.tile-row > * {
  flex: 1;
}

像这种容器(带有背景图片或者背景颜色的),它们之间的间隔设置往往很简单。如果需要调整文本行之间的间距,例如段落或者标题中的文本,可能会略显麻烦(a little more finicky)。

12.1.2 对行高的深入思考

Factoring in line height

设计稿中的文字周围还设置了一些间距,具体大小如图 12.3 所示(这里可能有点看不清楚,图中的内容栏是一块白色区域,其所在容器的背景则是很浅的浅灰色。文字区域的顶部和左侧到分栏边缘的距离均为 25px)。

图 12.3 正文板块内部和文字周围需要设置的间距
图 12.3 正文板块内部和文字周围需要设置的间距

文字周围环绕的 25px 间隔,可以考虑为板块添加内边距来实现。为了让该间隔对更大的字体作出响应,以实现同步呼吸的效果来确保良好的可读性,这里的间距选择使用相对单位 em。经计算,25px / 16px = 1.5625em

而标题与正文段落间的 30px 就没那么简单了。如果在二者之间指定 30px 的外边距,那么它们的实际间距会接近 36px。要理解为什么会这样,需要先来看看元素的高度究竟是如何定义的。

在盒模型中,元素的内容盒(content box)被该元素的内边距环绕,再往外则是元素边框,最后是其外边距。但对于这些元素而言,内容盒可不仅仅只有渲染出的文字部分;真正决定内容盒最终高度的,是元素的行高。这个高度覆盖了文字的顶部与底边,如图 12.4 所示。可以看到,当前文字的高度为 1em,而行高略微超出了文字的上下边缘。

图 12.4 行高定义了内容盒的高度
图 12.4 行高定义了内容盒的高度

示例页上的行高为 1.4,这是从 <body> 元素指定并继承过来的。这样一来,对于只有一行的文本元素,其内容盒的行高就是 1.4em,而文字则基于该行高垂直居中对齐;又因为当前字号为 16px,因此内容盒的最终高度即为 22.4px,多出的 6.4px 则平均分配到文字的上方和下方。

因此,如果为标题设置 30px 的底部外边距,则外边距的顶部与标题文字间实际将多出 3.2px 的间距,并且下方段落的内容盒也会多出来 3.2px(超出的间距是相等的,因为标题和段落具有相同的行高和字号)。这就导致了标题文本和正文段落间的实际间隔为 36.4px

注意

印刷领域的设计师习惯使用 行距(leading 来表示每行文字之间的间距。而在 CSS 中,文字间距是由行高(line height)控制的,不能直接等同为行距。

设计师通常不会在意一两个像素的差异,但多出 6½ 个像素就有点过分了。如果行高比现在还要高,或者元素设置了更大的字号,这样的尺寸偏差还会更加显著。

想要从根本上消除这样的差异,就需要算出多出的间距并在外边距中予以扣除。外边距不能指定为 30px 了,需要扣除 6px,变为 24px;再除以 16px,从而得到相对单位下的 1.5em。根据下列代码清单 12.3 所示内容同步更新本地样式表:

代码清单 12.3 设置内容板块和段落之间的间距

@layer global {
  p {
    margin-block: 1.5em; /* 为段落添加外边距 */
  }
}

/* ... */

@layer modules {
.tile {
  background-color: var(--white);
  border-radius: 0.3em;
  padding: 1.5625em; /* 在内容板块的内部指定内边距 */
}
.tile > h4 { /* 在内容板块的标题下方设置外边距 */
  margin-block: 0 1.5em;
}

上述代码中,外边距 1.5em 已在全局生效,因此页面中的所有段落都将具有相同的间距。然后在内容板块的标题下方(即 .tile > h4 元素下方)重复该操作,这样即便后面没有段落内容,标题下方的间距都将保持不变。由于存在外边距折叠效应,这两个外边距会相互重叠,从而在标题和段落间产生一个标准的 30px 间距。

说明

CSS 即将推出一个全新的属性 leading-trim,用于消除内容盒上下边缘的多余间距,不过该属性目前尚未获得任何浏览器的官方支持。想了解更多 leading-trim 的相关介绍,可以参考 Ethan Wang 刊登在 Medium 网站的这篇文章《Leading-Trim: The Future of Digital Typesetting》(https://mng.bz/67PR)。

译注:

截至 2024 年 12 月 10 日 Can I Use 网站最新的统计资料,各浏览器对 leading-trim 属性的支持率仅为 0.09%,目前仅 Safari 浏览器提供了相关支持;不过后续进展非常值得关注。

补图1:Can I Use 网站公布的各大浏览器对 leading-trim 属性的支持情况(截至 2024 年 12 月 10 日)

【补图 1:Can I Use 网站公布的各大浏览器对 leading-trim 属性的支持情况(截至 2024 年 12 月 10 日)】

视觉稿还需转换的最后一处间距,位于巨幅主图中的标语附近,如图 12.5 所示:

图 12.5 设计稿要求在标语上方预留 95px 的间距,而在其下方预留 16px 的间距(即标语和按钮之间)
图 12.5 设计稿要求在标语上方预留 95 px 的间距,而在其下方预留 16 px 的间距(即标语和按钮之间)

标语处的行高同样会影响到这些间距的设置,因为它的字号更大。标语处的字号为 1.95rem,乘以基准字号 16px,即 31.2px;再乘以 1.4 倍的行高,算得的最终行高大小为 43.68px,因此会在文字上下分别多出约 6px 的间距。

既然行高已经在文字上方占据了 6px,因此只需再增加 89px 的间隔就能满足上方间距的要求;同理,标语下方也只需再增加 10px 的间距,就能满足视觉稿中标语下方的间距要求。由于其间涉及大量的算术运算,最简单的解决办法通常是和设计师一起坐下来,在浏览器中实时修改这些间距值,直到获得设计师的首肯。

译注

为方便对照,这里直接给出示例页主图部分的 HTML 片段:

<div class="hero">
  <div class="hero__inner">
    <h2>Team collaboration done right</h2>
    <a href="/sign-up" class="button button--cta">Get started</a>
  </div>
</div>

明确要在标语上下位置添加的具体间距尺寸后,就可以将其更新到样式表中了。具体样式详见代码清单 12.4。根据给出的内容同步更新本地样式表,变更位置已添加注释说明。此外还在 hero__inner 容器上设置了最小高度(译注:即 min-block-size),以确保能像设计稿那样撑开一大块屏幕空间。

代码清单 12.4 在主图中设置标语和按钮的位置

.hero {
  background: url(collaboration.jpg) no-repeat;
  background-size: cover;
  margin-bottom: 40px;
}
.hero__inner {
  max-inline-size: 1080px;
  margin-inline: auto;
  min-block-size: 40svh;
  padding-block: 89px; /* 用新计算的间距值替换此前的估值 */
  padding-inline-end: 12.5em;
  text-align: right;
}
.hero h2 {
  margin-block: 0 10px; /* 定义标语和按钮间的间距大小 */
  font-size: 1.95rem;
}

hero__inner 的顶部内边距定义了标语上方所需的间距。尽管设计稿中并没有标注其右侧的内边距尺寸,但上述代码中也进行了相应设置(译注:即 12.5em)。最后将标语元素的顶部外边距设为 0,这样就不用担心在 hero__inner 的内边距上再多出额外的间距了。

译注
在复盘本篇要点时,偶然发现作者提到的那篇介绍 leading-trim 属性的文章没有完整的翻译版,要么是节选的,要么是收费的(有点过分了)。拟计划完成本章所有内容后,再来翻译这篇文章并放到本专栏。敬请关注!

12.1.3 行内元素的间距设置

Spacing inline elements

设计稿中还有最后一处细节样式需要微调,即中间的内容栏展示的操作系统列表区域。示例应用 Ink 可以在这些操作系统中使用(如图 12.6 所示)。之前将每个列表项放到了一个无序列表元素(<ul>)中,现在需要根据设计稿的要求将它们排成一行。

图 12.6 列表项需要微调样式并在一行内展示
图 12.6 列表项需要微调样式并在一行内展示

这种微型布局的设计在一些标签类的内容展示上十分常见,例如博客文章的标签列表或商品类别等。本例之所以采用这样的设计方案,旨在带您了解几个您应该比较熟悉且感兴趣的小问题(quirks)。

这种布局有多种实现方案,Flexbox 弹性盒布局和行内元素是两种比较容易想到的。本书之前的章节已经介绍了不少有关 Flexbox 布局的知识,因此这里就重点考察一下采用行内元素时需要考虑哪些问题。

如果采用行内元素来实现上述效果,应该很容易想到一些样式。例如每个元素项都需要声明 display: inline;各元素还需要设置一些内边距、背景色、圆角边框等等。一开始可能感觉只要有这些样式就够了,可一旦内容中出现换行的情况,问题便接踵而至:当视口宽度为某个固定值,或者后续列表项发生了变化,就可能会发生如图 12.7 所示的情况:

图 12.7 多个列表项在换行时出现了重叠的情况
图 12.7 多个列表项在换行时出现了重叠的情况

每一行列表项的灰色背景会和另一行的列表项重叠,原因就在于行高。前面讲过,文本行的高度是由 字号 乘以 行高 决定的。如果为行内元素添加内边距,元素本身虽然也会变高,但不会增加文本行的高度。文本行的高度完全由行高决定。

要解决这个问题,就需要增加每个列表项的行高。代码清单 12.5 给出了标签列表模块对应的 CSS 代码。将它们同步更新到 modules 模块图层对应的样式表中。也可以尝试不同的行高,看看会产生什么样的效果。

译注
为方便对照,这里直接给出正文栏中间那栏的 HTML 标记:

<div class="tile">
  <h4>Take it with you</h4>
  <p>
    Ink is available on a wide array of devices, so you can work from
    anywhere:
  </p>
  <ul class="tag-list">
    <li>Web</li>
    <li>iOS</li>
    <li>Android</li>
    <li>Windows Phone</li>
  </ul>
  <a href="/supported-devices" class="button">Read more</a>
</div>

代码清单 12.5 为标签添加样式

@layer modules {
  .tag-list {
    /* 覆盖用户代理默认的列表样式 */
    list-style: none;
    padding-inline-start: unset;
  }
  .tag-list > li {
    display: inline;
    padding: 0.3rem 0.5rem;
    font-size: 0.8rem;
    border-radius: 0.2rem;
    background-color: var(--light-gray);
    line-height: 2.6; /* 设置一个较大的行高,以便在换行时有足够的垂直空间 */
  }
}

值得一提的是,只有行内元素才具有这样的行为模式。如果一个元素是弹性子元素(或 inline-block,即行内块级元素),为了容纳它,其所在的行也会随之增高。当然页面仍需设置水平和垂直外边距来增加子元素间的间隔。借助行内元素的相关特性,就能通过元素间的天然空白来产生想要的间距。

注意
请注意页面中 “Windows Phone” 字样的子元素,它是可以换行的。如果换成弹性盒子或者行内块(inline block)中,则不允许像这样换行,整个元素都将换到下一行显示。如果不允许出现这样的情况,则需要根据实际情况选择最适合您的方式来解决这个问题。

这样我们就根据上一章给出的设计稿完成了整个页面的实现。本地页面的最终效果应该和图 12.8 中展示的视觉稿效果完全一致。

图 12.8 完成页面设计后的效果
图 12.8 完成页面设计后的效果

我们花了很多时间来分析这些样式细节。很多开发者在实现页面设计时往往不太在意这些细节,但对于那些关注了细节的开发者来说,做到就相当于赚到。往往也正是这些细节道出了普通与优秀之间的本质差别。

在开发 CSS 样式时,建议大家多花些时间来完善设计细节。即便您做得没有设计师那么专业,也要相信自己的眼光。试着在这里多预留些空间,或者在那里少留一点,看看哪种效果更好。要舍得花时间来调试,但千万别滥用页面颜色,而是有选择地将颜色放在最需要吸引用户注意的位置。要创建一致的模式,然后打破这些规律的模式,这样才能将用户的注意力吸引到页面最重要的内容上。

12.2 Web 字体

Web fonts

接下来,我们将通过添加自定义字体来让页面效果再上一个新台阶。迄今为止,我都没在示例页面的设计稿中囊括这些内容,但设计师几乎肯定会在其设计作品中指定网页字体。

页面设计可谓成也字体,败也字体。多年来,Web 开发者只能从很有限的字体库中做选择,即所谓的 Web 安全字体(web-safe fonts。这些字体类型(font families)包括 ArialHelveticaGeorgia 等,且绝大多数用户的操作系统都会安装。早期的浏览器只能使用这些系统字体来渲染页面,因此也不得不使用它们来进行网页开发。即便偶尔也可能指定某个非系统字体,比如 Helvetica Neue,但也只有那些碰巧安装了这款字体的用户才能正确渲染,而其他用户只能看到更通用的字体回退方案。

随着 Web 字体的兴起,情况改变了。Web 字体可以通过 @font-face 规则来告诉浏览器去哪儿检索并下载页面所需的自定义字体。使用自定义字体后,原本平淡无奇的页面也可以大有改观,仿佛开启了一个全新的世界,让人们有了更多选择与想象的空间。

不同的页面字体会带来不同的观感体验,或灵动活泼、或有板有眼;或沉稳可信、或不拘一格。如图 12.9 所示,对同一段文字分别使用了三组不同的字体。在左上角的示例中,标题用的是 News Cycle 字体,而正文则用的是 EB Garamond;这样看起来比较正式,一般出现在新闻报纸类网站中。而右上角的案例则分别使用了 ForumOpen Sans 字体,视觉效果就没那么正式,可用于个人博客或者小型技术公司。最后左下方的案例用到的字体分别是 AntonPangolin,显得比较活泼,甚至略带卡通风格,适用于儿童类网站。以上三个案例,仅仅只是变更了一下字体,别的什么都没做,就可以营造出截然不同的页面氛围。

图 12.9 不同的字体会显著影响网站的整体观感
图 12.9 不同的字体会显著影响网站的整体观感

通过在线服务来使用 Web 字体是最简单、可能也是最普遍的方式。常见的有谷歌字体Adobe 字体

无论收费还是免费,这些服务都可以解决很多问题,包括技术上(如托管服务)和法律上(如授权许可)的一些问题。它们都提供了大量可供选择的字体库;但偶尔也遇到某个特殊字体仅对付费用户开放,并且还得自行处理托管事宜。

由于谷歌字体提供了很多高质量且开源的字体(而且是免费的),接下来我将带您了解利用这项服务将 Web 字体添加到页面的具体方法。因为谷歌做了大量的工作,所以添加字体非常简单。然后我们将深入研究它的底层运行逻辑,看看它究竟是怎么工作的、又是如何托管自己的字体的。

本章将沿用之前构建的示例页,并通过 Web 字体来完善页面设计。完成后的页面渲染效果如图 12.10 所示。图中大部分内容用的是 Roboto 字体,它也是整个页面主要的正文字体;而标题使用的是 Sansita 字体。

图 12.10 启用了 Sansita 和 Roboto 这两款 Web 字体的页面局部效果

【图 12.10 启用了 Sansita 和 Roboto 这两款 Web 字体的页面局部效果】

让标题和正文分别使用两种不同的字体是很常见的做法。通常情况下,其中一种字体是 衬线(serif)字体,而另一种则为 无衬线(sans serif)字体。但本例中的两种字体都是无衬线类型。此外还可能见到有的设计标题和正文用的是相同的字体,但字体粗细各不相同的情况。

定义
衬线(serif 是某字符笔画末端的小线条或者“爪状”修饰效果。包含衬线的字体就被称为 衬线字体(serif typeface)(例如 Times New Roman);而没有衬线效果的字体则被称为 无衬线字体(sans serif typeface(例如 Helvetica)。

12.3 谷歌字体

Google fonts

访问谷歌字体网站,就可以看到谷歌字体中当前可用的字体目录。该网站通过网格板块的方式来展示这些字体(如图 12.11 所示)。您可以直接从页面上选择字体,或者通过页面顶部的搜索栏或左侧的筛选工具来检索特定字体。

图 12.11 谷歌字体的字体选择界面
图 12.11 谷歌字体的字体选择界面

单击其中某个字体将显示更多详情信息,包括可用的字体粗细(细体、常规、粗体)及字体样式(常规或斜体)的列表。在屏幕的右上角有个获取该字体(“Get font” 字样)的按钮。点击该按钮会将其添加到已选字体中,并可通过点击右上角的小购物袋图标来预览选中的字体。

如果知道需要什么样的字体,就可以通过字体名称来快速检索。在搜索栏中输入 Sansita 后,就会从主视图中筛选掉其他无关字体。点击 Sansita 字体进入详情页,然后点击获取字体按钮(即 “Get font” 字样的按钮)将其添加到已选字体中。这样相当于添加了该字体的所有粗细和样式,稍后再做进一步细化。

然后返回谷歌字体首页(点击顶部的 Google Fonts 图标)。在搜索框输入 Roboto,Google 会找出好几种相关字体,其中包括 RobotoRoboto CondensedRoboto Slab 等。点击 Roboto 字体并将其添加到已选字体中。

这样一来,页面就会显示 “2 font families selected” 字样(即 “已选中两个字体类型”)。页面右侧有两个按钮,按钮 “Get Embed Code”(获取嵌入式代码)负责提供嵌入这些字体所需的 CSS(如图 12.12 所示);而按钮 “Download all”(即 “全部下载”)则可以将所选字体下载到本地。我们先点击第一个按钮。

图 12.12 左侧为当前选中的字体列表,右侧则显示对应的嵌入式代码
图 12.12 左侧为当前选中的字体列表,右侧则显示对应的嵌入式代码

到现在为止,您已经熟悉了常规和粗体字的粗细设置,但某些字体还支持多种不同的粗细尺寸。例如,Roboto 字体共有六种不同的粗细,范围从细体(thin)到特粗体(black)不等,同时每一种粗细还有对应的斜体字格式。请注意,除了使用细体(thin)、常规(regular)、粗体(bold)、黑体(black)外,字体粗细(weights)还可以用数值来表示,例如 100、400、700 和 900 等等。

说明
字型(typeface字体(font 这两个术语经常被混为一谈。字型 是指字体的整个家族(例如 Roboto),通常由同一位设计师创建;一种字型可能会存在多种样式变体或粗细形式(例如细体(light)、粗体(bold)、斜体(italic)、压缩(condensed)等等)。这些变体的每一个都可以称之为一种 字体(font

理想情况下,您可以选择字体所有的变体形式来为您的页面设计提供丰富的选择。然而,要添加的字体越多,意味着浏览器要同步下载的内容也会增多,从而让 Web 字体成为仅次于图片的、拖慢页面加载速度的几个罪魁祸首之一。为此,一种名为 可变字体(variable fonts 的全新技术应运而生,可以在一个更轻量的文件中实现多个变体形式的嵌入,本章稍后会为您演示其用法;但要是该方案并不具备实施条件,就应该更加慎重,只选取真正需要的字体。

一开始,字体的所有粗细版本都会被选中。在每个选中的字体下方,点击 “Change styles” 按钮(即 “变更样式”),并取消选中除了 Roboto 中的 Light 300、以及 Sansita 中的 ExtraBold 800 以外的所有粗细样式。剩下的这两个才是页面需要的字体粗细。通常还需要准备正文主体文字的斜体字版本,但在本例中无需考虑。

然后根据代码清单 12.6 所示的样式,将 <link> 标签复制到示例页的 <head> 元素中。这样就把包含字体描述的样式表添加到了示例页。此时页面会有两个样式表:一个是您自己的,而另一个则是字体的专属样式表。

此外,代码中还包含两个设置了 preconnect 预连接的 <link> 元素。它们是给浏览器的提示信息,用于告诉浏览器 Web 字体需要加载来自如下 Google 域名的资源,以便在解析 HTML 时预先进行连接,从而让样式表在后续解析到它们时,进一步提升字体的加载速度。

代码清单 12.6 引入谷歌字体样式的 <link> 标签写法

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300&family=Sansita:wght@800&display=swap"
  rel="stylesheet"
/>

有了这个样式表,Web 字体的设置就能完全交给 Google 打理了。设置完成后就能在样式中使用这些字体了。将上述代码添加到本地页面,将得到如图 12.13 所示的页面效果:

图 12.13 使用了 Roboto 字体及 Sansita 字体后的页面效果
图 12.13 使用了 Roboto 字体及 Sansita 字体后的页面效果

要让相应的字体生效,需要通过 font-family 属性来指定 Roboto 字体或 Sansita 字体。为此,得再更新一下 CSS。先在 <body> 标签中指定正文字体 Roboto,这样整个网页都将继承该字体;然后将各级标题的字体、以及首页连接元素 home-link 的字体均设置为 Sansita。最后根据下列样式代码更新示例页面即可(要改动的位置已用注释说明)。

代码清单 12.7 使用 Web 字体的示例样式代码

@layer global {
  body {
    margin: 0;
    font-family: Roboto, sans-serif; /* 对页面全局应用 Roboto 字体 */
    line-height: 1.4;
    background-color: var(--extra-light-gray);
  }

  h1, h2, h3, h4 {
    font-family: Sansita, serif; /* 各级标题设为 Sansita 字体 */
  }
}

/* ... */

@layer modules {
  .home-link {
    color: var(--text-color);
    font-size: 1.6rem;
    font-family: Sansita, serif; /* 左上角的首页链接设为 Sansita 字体 */
    font-weight: bold;
    text-decoration: none;
  }
}

由于页面添加了谷歌字体样式表,浏览器就能正确解析这些字体设置并将其关联到已下载的 Web 字体上,最终让对应字体生效。如果使用其他的 Web 字体服务(例如 Adobe 字体),那么整个过程也是大同小异。这些服务要么提供所需 CSS 样式的 URL 地址,要么提供能为页面添加 CSS 样式的 JavaScript 代码片段。

接下来我们将对字体间距进行微调,并分享几个关于性能加载方面的考量因素。在这之前,先来看看谷歌字体都为我们做了哪些工作。

12.4 @font-face 的工作原理及用法

How @font-face works

提供字体服务的网站把添加字体的工作做得如此简单易用,但我们仍然有必要了解一下它们是怎么实现的。先来看看谷歌提供的 CSS 文件。在浏览器中打开 URL,就可以看到对应字体的 CSS 样式。我们复制其中的一部分,如代码清单 12.8 所示:

代码清单 12.8 定义谷歌字体的示例样式代码

/* 拉丁字符 */
@font-face { /* 每条 @font-face 规则定义一个字体,可在页面其他 CSS 中使用 */
  font-family: 'Roboto'; /* 声明该字体的名称 */
  font-style: normal; /* 定义该 @font-face 规则使用的字体样式 */
  font-weight: 300; /* 定义该 @font-face 规则使用的字体粗细 */
  font-display: swap;
  /* 指明字体文件的具体位置 */
  src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fBBc4AMP6lQ.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 {
  font-family: 'Sansita';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/sansita/v11/QldLNTRRphEb_-V7JLmXWX5
   -w4dsz_k.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 规则定义了可在页面 CSS 中使用浏览器字体。这里的第一条规则实际上是说,“如果页面需要渲染 font-familyRoboto 的拉丁字符,并且这些字符使用了常规的字体样式(非斜体字)、字体粗细为 300,那么就使用该字体文件”。同理,第二条规则定义了一个字体粗细为 800 的粗体版 Sansita 字体。

font-family 则定义了引用字体的名称,以便在样式表的其他地方生效。src 属性则提供了让浏览器下载该字体的具体位置。

而设置 unicode-range 属性并非 @font-face 正常工作的必要条件。谷歌将其用于性能优化:将最常用的拉丁字符放在一个较小的字体文件中,而将其他字符的扩展集(extended sets)放在额外的文件中。这样,拉丁字符就可以快速下载,而扩展集仅在页面需要时才被浏览器下载。扩展字符集会在谷歌样式表中各自获取对应的 @font-face 规则,例如西里尔字符(Cyrillic)、希腊字符(Greek)、越语字符(Vietnamese)等等。鉴于其基本原理是共通的,因此为确保简洁,代码清单 12.8 中省略了这部分内容。

这里的关键点在于,如果要使用的是线上暂不提供托管服务的付费版字体,您至少得知道如何定义自己的 @font-face 规则。通常,我会先用谷歌字体检索出自己喜欢的、有免费许可的字体,然后将它们下载下来并自行托管。

12.4.1 字体格式与回退处理

Font formats and fallbacks

谷歌提供的这些字体文件都采用了一种名为 WOFF2 的格式。WOFFWeb Open Font Format(Web 开发字体格式)的缩写,是一种专为 Web 使用而设计的字体压缩格式。当前所有的现代浏览器都支持 WOFF2 格式,而更新、更好的字体格式还会定期出现。为了应对这种局面,比较稳健的解决方案应该是同时给 WOFF2 与后续的新字体格式提供 URL。代码清单 12.9 假设了一种预想的 WOFF3 格式,并演示了如何基于浏览器的实际支持情况来同时提供这两种配置方案(为提高代码的可读性,这里采用了简化版的 URL)。

代码清单 12.9 支持回退到另一种格式的 Web 字体声明

@font-face {
  font-family: "Roboto";
  font-style: normal;
  font-weight: 300;
  src: local("Roboto Light"), local("Roboto-Light"),
       url(https://example.com/roboto.woff3) format('woff3'), /* 使用列表中支持的首个字体格式 */
       url(https://example.com/roboto.woff2) format('woff2'); /* 不支持 WOFF3 字体的浏览器将回退至 WOFF2 格式 */
}

以上示例代码还用到了一个特殊的 local() 函数,用于指示浏览器先检查用户操作系统中是否已经安装了给定名称的字体。如果确实安装了,则启用该字体;否则将发起一个网络请求下载对应字体。

Web 字体刚刚兴起的时候,由于各个浏览器支持的字体格式参差不齐,开发者不得不引入四五种不同格式的字体;而现在,WOFF2 格式已经得到了各浏览器厂商的广泛支持,因此除非出现更新的字体格式,通常情况下无需考虑上述写法。

12.4.2 同一字型的多种变体形式

Multiple variants of the same typeface

如果要用到同种字型(typeface)下的多种字体,那么每一种字体都需要定义各自的 @font-face 规则。如果在谷歌字体界面上同时选取了 Roboto 字型的细体版和粗体版,谷歌就会提供一个形如 https://fonts.googleapis.com/css2?family=Roboto:wght@300;700&display=swap 的样式表 URL。在浏览器中打开该 URL 并查看样式代码,这里截取了其中一部分,如代码清单 12.10 所示:

代码清单 12.10 定义同一字型下两种不同粗细的字体

/* 拉丁字符 */
@font-face {
  font-family: 'Roboto'; /* 细体 Roboto */
  font-style: normal; /* 细体 Roboto */
  font-weight: 300; /* 细体 Roboto */
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fBBc4AMP6lQ.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 {
  font-family: 'Roboto'; /* 粗体 Roboto */
  font-style: normal; /* 粗体 Roboto */
  font-weight: 700; /* 粗体 Roboto */
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmWUlfBBc4AMP6lQ.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;
}

上述代码片段展示了两种不同 Roboto 字体的定义方法。如果页面需要渲染字体粗细为 300Roboto 字体,则会启用第一种定义;若是需要渲染字体粗细为 700Roboto 字体,则会启用第二种。

但如果页面样式需要用到其他版本的字体(如 font-weight: 500 或者 font-style: italic),浏览器则会尽可能从已有的两种字体定义做近似选择。也就是说,通常情况下浏览器会选择与所需字体更接近的版本。不过这取决于浏览器,偶尔也可能会把某个已有字体变为斜体或者加粗来趋近预期的效果;例如通过使用几何学方法来实现,产生 “伪加粗”(“faux bold”)或者 “伪斜体字”(“faux italic”)效果。但这样的效果肯定不如原生设计的字体效果好。因此,遇到只能使用 Web 字体中仅限 @font-face 规则声明的字体粗细与样式的情况时,您应当慎之又慎。

在使用谷歌字体或者其他字体服务提供商时,通过界面操作就可以获取到所需的代码。有时可能服务商没有提供您要找的字体,此时就需要自己搭建字体服务,利用 @font-face 规则来定义浏览器需要的字体格式。

12.5 性能因素考量

在使用 Web 字体时,需要考虑一下性能问题,因为字体文件很大。之前提过,页面用到的字体文件数量应该精简到最少;但即便如此,仍然可能出现问题。在浏览器中时不时会遇到这样的情况:眼看页面内容和样式布局就要开始渲染了,字体却还在不停地下载。对于这段短暂的时间延迟,深入理解其间都发生了什么至关重要。

先来考察第一种情况:本地已经安装了浏览器备用的系统字体,而此时的页面正等待目标字体加载完毕。图 12.14 演示了这种情况。

图 12.14 FOUT(无样式本文闪动)页面效果示意图
图 12.14 FOUT(无样式本文闪烁)页面效果示意图

相较系统字体,Web 字体很可能会在屏幕上占据不一样的空间。等到第二次渲染时,一旦页面布局发生改变,文字也会随之在页面上突然跳动。如果这是在页面第一次渲染后很快发生的,用户可能都察觉不到;但要是字体在下载过程中存在网络延迟(或者字体文件本身过大),则可能要经历长达数秒的等待之后才能再次渲染页面。当出现这种情况时,有些用户可能会有厌烦情绪。他们很可能已经开始阅读网页内容了,结果页面突然一变,就扰乱了刚才的注意力。这就是所谓的 FOUT,即无样式文本闪烁(Flash of Unstyled Text)。

为了避免出现这样的情况,浏览器厂商采用了另一种渲染方式:浏览器不再渲染备用字体,而是直接渲染除页面文字以外的其他所有内容。确切地说,是把文字渲染为不可见内容(invisible),使其仍会占据相应的页面空间。这样一来,容器在页面上的定位就固定下来了,而用户则可以看到当前页面正在加载。这就导致了一个新的问题:FOIT,即不可见文本闪烁(Flash of Invisible Text)。如图 12.15 所示,背景颜色和边框样式已经渲染出来了,但文字仅在页面进行第二次渲染时才会显示,即 Web 字体加载完毕后。

图 12.15 FOIT(不可见文本闪烁)示意图
图 12.15 FOIT(不可见文本闪烁)示意图

该方案虽然解决了之前的问题,但同时又引出了新的问题:如果 Web 字体加载时间过长怎么办?要是字体加载失败,情况又如何?倘真如此,页面会一直呈现空白,徒留一堆在用户看来毫无意义的彩色方框。这样,人们也只能用回 FOUT 时的系统字体。

事实已经明摆着了:FOUTFOIT 都不尽如人意。并且在 Web 字体领域,这两个问题一直未能得到彻底解决。我们能做的不过是尽可能地将它们产生的负面影响降至最低。

限制网速来测试字体加载行为

如果是在网速较好时开发项目,人们将很难测试到网站的字体加载行为。有一种解决方案是在 Chrome 或者 Firefox 的开发者工具中手动下调网速。

ChromeNetworks 标签页中,顶部工具栏里有个下拉菜单,里面预设了几种网速。选中下拉菜单中的 Regular 3 G (常规 3 G 网络)选项,就可以手动设置低网速模式,如下图所示:

浏览器开发者工具 DevTools 中的网络限流工具

【浏览器开发者工具 DevTools 中的网络限流工具】

建议同时勾选 Disable Cache(禁用缓存)旁边的复选框,每次加载页面,所有资源都会重新下载。这样就可以模拟出用户在低网速条件下看到的、网站页面初始加载时的情况。

这些设置仅在 DevTools 开发者工具处于开启状态时才会生效。完成测试后记得将上述配置还原到正常模式下,否则当您下次打开 DevTools 时可能会大吃一惊。

12.5.1 font-display 属性解析

The font-display property

font-display 属性可用于指定浏览器在处理字体加载问题时的具体方式。该属性位于 @font-face 规则内部。

在谷歌字体的样式表中(详见代码清单 12.8、12.10),您可能已经注意到了,每个字体规则内都包含了一句声明 font-display: swap。它用于指示浏览器立即显示备用字体、并且在 Web 字体可用时再切换(swap)过来——简言之,即采用 FOUT 方案。

该属性的其他合法取值如下:

当网速够快时,最好选用 fallback,这样能提供短暂的 FOIT,但如果 Web 字体的加载时间超过了 100ms,则会产生 FOUT;而当网速较慢时,选择 swap 会好一些,这样可以立即渲染备用字体;要是 Web 字体对于整体设计而言并非不可或缺,则可以考虑使用 optional

此外,在谷歌字体提供的样式表 URL 中还包含了一个查询参数 &display=swap。该参数会在生成的字体样式表中声明 font-display: swap。您也可以利用上面介绍的属性值来手动修改字体的显示属性。

如何控制 Web 字体的渲染性能是个比较棘手的问题。如果想要进一步深入研究,推荐阅读 Jeremy L. Wagner 写的《Web 性能实战》一书(即 Web Performance in Action,Manning 出版社 2016 年 12 月出版发行)。书中用了一整章篇幅来介绍 Web 字体的相关性能,同时部分章节也涉及其他与 CSS 相关的话题。

更多与字体相关的相对单位

值得一提的是,除了 emrem 外,CSS 还提供了一些与字体相关的相对单位。

例如行高相对单位 lh1lh 相当于当前元素的行高。因此,对于一个字号为 16px、行高为 1.5 的元素而言,声明 margin-block: 1lh 将得到大小均为 24px 的上下外边距。截止 2023 年底,所有主流浏览器已经提供了对相对单位 lh 的支持。此外还有一个即将推出的相对单位 rlh1rlh 相当于根节点元素(即 <html>)的行高值。

此外还有字符相对单位 ch1ch 相当于当前字体下、字符 0 所占用的宽度大小,尤其适合与等宽字体结合起来使用。例如可以通过声明 width: 80ch 来将容器宽度限制为 80 个文本字符宽度。

最后再介绍一个与特定字符高度相关的相对单位 ex(the x-height unit)。1ex 相当于当前字体下、小写字母 x 所占用的高度,亦即小写字母中去掉上升部分(ascenders)的高度,例如字母 x。该相对单位在不同的字体间具有不同的大小,通常 1ex 接近或介于 0.4em0.6em 不等。而在一些风格化的字体(译注:即 stylistic typefaces,如手写体、艺术字等)中,相对单位 ex 的具体大小可能会存在相当大的悬殊。

在使用 Web 字体时,通常最好避免使用像 chex 这样的相对单位来设置页面大部分的内容。由于这些相对单位的取值在不同的字体下不尽相同,一旦页面上的 Web 字体加载完毕,很可能会出现大量的布局偏移(layout shift)。

Web 字体看起来很不错,可以为页面增色不少,只可惜它们在性能上的表现始终无法媲美系统安装的字体。如果想使用系统字体,Modern Font Stacks 网站提供了一组实用的字体堆栈(font stacks 1)列表,可在各大操作系统中使用。

12.5.2 可变字体的用法

Variable fonts

当前还有一些全新的字体格式供人们使用,例如本节要介绍的 可变字体(variable fonts。与其将细体字、常规尺寸及粗体字分别放入不同的字体文件中,不如将所有的粗细版本都包含到同一个字体文件内。通常情况下,可变字体的文件大小甚至还要比同一字体下的多个独立版本文件小得多。可变字体也因此成为设置页面字体多种粗细版本的一种更为高效的解决方案。

可变字体通过在字体文件中定义一个或多个 变体轴(variation axes 来实现相关功能。变体轴描述了该字体某一特定样式维度的许用范围(allowable range)。最常见的变体轴即 “字体粗细轴”(“weight axis”),它定义了浏览器控制字体由细到粗进行转换的具体方式及范围。字体粗细的取值范围介于 0 到 999 不等。

此外,变体轴还可以实现类似 “开启” 或 “关闭” 的二元选择功能,一个典型的应用即 “斜体轴”(“italic axis”),用于控制字体是按常规方式渲染、还是按斜体字渲染,其间没有过渡状态。

代码清单 12.11 给出了可变字体 Open Sans@font-face 规则定义。其中字体粗细的取值范围介于 300800 之间。注意示例代码中的 font-weight 属性有两个值,分别定义了字体粗细可用的最小与最大值。

代码清单 12.11 使用了可变字体的 @font-face 规则示例

@font-face {
  font-family: "Open Sans";
  font-style: normal;
  font-weight: 300 800; /* 指定字体粗细介于 300 到 800 之间 */
  font-display: swap;
  src: url(./open-sans.woff2) format("woff2");
}

有了上述 @font-face 规则,页面就可以使用 Open Sans 字体系列(font family)了,字体粗细的取值范围介于 300(细体字)到 800(特粗体)不等。此时没有 100 整数倍的限制,可以放心使用精确的粗细值,例如 font-weight: 650 或者 font-weight: 467 等等,只要不超过指定范围且符合 @font-face 规则定义即可。

12.5.2.1 字体的变体轴

Font variation axes

利用 font-weight 属性来设置可变字体的粗细通常是最简单的方法。此外也可以通过更底层的语法规则、利用 font-variation-setting 属性来实现相同的效果,如以下代码所示。示例代码基于代码清单 12.11 定义的 Open Sans 字体,实现了一组等效的样式声明:

p {
  font-family: 'Open Sans', sans-serif;
  font-weight: 400;
}

p {
  font-family: 'Open Sans', sans-serif;
  font-variation-setting: 'wght' 400;
}

上述代码中,font-variation-setting 属性引用了一个由四个字符组成的特殊字符串 'wght'。该字符串用于指代字体文件中的字体粗细变体轴。变体轴始终由四个字符组成的特殊字符串来定义。目前可变字体可以引用五个已注册的变体轴(registered axes),它们分别对应于字体样式的某一变体属性,具体映射情况如下:

同时支持两个或两个以上变体轴设置的可变字体较为少见。有关所有这些变体轴的交互案例,详见 MDN 在线文档:https://mng.bz/ZEDP

除了这五个已注册的变体轴外,字体设计师还可以向字体添加 自定义变体轴(custom axis。自定义变体轴使用四个大写字符组成的字符串来定义,例如 'GRAD'。自定义变体轴无法映射到其他 CSS 属性(property),只能通过 font-variation-setting 进行调整。如果遇到使用了自定义变体轴的可变字体,需要查看该字体的文档来获取其具体用法。

注意
一些字体内置了调色板功能,可以使字形的不同部分呈现不同的颜色。此时可以利用 CSS 属性来修改这些颜色。这是一个相对小众的应用场景,因此在本书中不作详细介绍。更多实现详情,可以参考这篇文章:https://www.matuzo.at/blog/2023/100daysof-day76/

许多字体托管服务,例如谷歌字体,都推出了多种可变字体供人们选用。建议在可能的情况下,尤其是当您预计会用到同一字体的多种变体样式时,试着去找找这些可变字体。

  1. 译注:所谓字体堆栈(font stacks),是指在网页设计和排版中,通过指定一系列字体的列表,以便浏览器根据可用字体的顺序选择和渲染文本,旨在确保文本在不同设备和环境中的可读性和一致性。例如:font-family: "Helvetica Neue", Arial, sans-serif; 就是一个字体堆栈。

12.6 调整字间距,提升可读性

Adjusting space for readability

让我们再回到页面上。此时的 Web 字体 RobotoSansita 已加载完毕,我们可以按照设计稿再调整一下。这里涉及两个属性(properties):line-heightletter-spacing。它们可以控制文本行之间的距离(垂直方向)和单个字符之间的距离(水平方向)。

很多开发者往往不太看重这两个属性。如果在页面开发过程中多花点时间调整它们,整个网站的外观都将得到显著改善。除此之外,还可以让用户阅读更加舒适,从而增加用户黏性。

如果文字间距太过紧凑,多读几句话甚至多看几个字都会明显感觉费劲;要是间距过大也会有同样的问题。图 12.16 展示了多个不同间距的文字版本。

图 12.16 文字间距会对阅读体验产生显著影响
图 12.16 文字间距会对阅读体验产生显著影响

试着读一下左上方的压缩版文字,就会发现需要更加集中注意力才行。可能一不小心就漏掉一行或者重复阅读同一行,而且很快就读不下去了。这样的页面显得拥挤不堪,毫无条理。而左下方的文字又过于分散了些,致使每个字母都占用了太多注意力,不太容易在大脑里组合成单词。相比之下,右上方的文字就舒服多了,看上去“刚刚好”,是这三个版本中最容易阅读的。

12.6.1 正文的字间距

Body copy spacing

line-heightletter-spacing 找到合适的值是件主观性很强的事。最好的解决方案通常是多试几个值;如果找到某两个值一个过于紧凑、另一个过于松散,那就取介于二者之间的某个值。所幸,下面介绍的这些经验法则可以为您提供帮助。

line-height 属性的初始值(initial value)是关键字 normal,大概等于 1.2(确切的数值是在字体文件中编码的,它们取决于字体的 em 大小);但是在大部分情况下,这个值偏小。对于正文内容而言,行高介于 1.41.6 之间较为理想。

我们已经在上一章为 <body> 元素设置了 1.4 倍行高。这个值会被页面中的其他元素继承。试想如果没有了这个行高值页面会怎样渲染。图 12.17 展示了其中一个板块的前后效果对比。在左侧的版本中,line-heightletter-spacing 属性均为初始值;而右侧的版本是调整后的效果(我们的目标是把字间距调整为右侧的版本)。

图 12.17 Ink 页面的一个板块效果对比,其中左侧为原始的字间距效果,右侧为手动调整后的效果
图 12.17 Ink 页面的一个板块效果对比,其中左侧为原始的字间距效果,右侧为手动调整后的效果

line-height 的值改为 1.3 或者 1.5,看看效果如何。是不是比之前 1.4 倍行距的效果好一些。

提示
一段文字越长,行高也应该相应越大。这样读者的眼睛才更容易扫到下一行,而不会分散注意力。理想情况下,每行文字的长度应该控制在 45 至 75 个字符之间,一般认为这样的长度最利于阅读。

接着再来看看 letter-spacing 属性。如果用的是精心设计过的字体,可能并不需要调整默认的字间距,但偶尔适当的调整也可以进一步提高可读性,因此还是有必要带您过一遍,看看如何进行修改。修改该属性的另一个应用场景还可以是出于风格方面的考虑,对页面上的某些位置(如按钮或标题)进行微调。

letter-spacing 属性需要一个长度值,用来设置每个字符间的间距。即使只设置 1px,也是很夸张的字间距了;因此它应该是一个非常小的值。在尝试找到最佳属性值的过程中,通常我会每次只增加 1em1/100(例如 letter-spacing: 0.01em)。请根据代码清单 12.12 同步更新本地样式表中的字间距。

代码清单 12.12 在 body 元素上设置字间距

@layer global {
  body {
    margin: 0;
    font-family: Roboto, sans-serif;
    line-height: 1.4; /* 行高和字间距将被页面上的所有元素继承 */
    letter-spacing: 0.01em; /* 在各字符之间再添加 0.01em 的字间距 */
    background-color: var(--extra-light-gray);
    color: var(--text-color);
  }
}

不妨尝试将字间距增至 0.02em0.03em,看看页面效果如何。您可能不具备设计师的专业眼光,没办法确定哪种效果更好;但是没关系,跟着感觉走就行了。如果还是有疑虑,那就保守一点,不要设置得太开。我们的目的不在于吸引用户注意字间距,而是恰恰相反。在 Ink 页面上,我发觉 0.01em0.02em 看着都不错,那就保守一点选用 0.01em

把行距和字距转换为 CSS 样式

在设计领域,文本行之间的距离称为 行距(leading,与单词 bedding 谐音。它起源于印刷版每行文字之间添加的一条条铅引导线。而字符之间的距离则称为 字距(tracking。如果与设计师一起工作,它们可能会在设计稿中指明行距和字距,但这些尺寸看起来和 CSS 中的 line-heightletter-spacing 属性完全不沾边。

行距一般以 “点(point)” 为单位进行描述,例如 18pt,代表的是一行文字的高度加上它与下一行文字之间的距离。这其实与 CSS 中的 line-height 类似,只不过没有用不带单位的数字来描述罢了。实际转换时必须像定义字号那样,先将行距转为像素尺寸,然后再计算出对应的不带单位的行高值。

而要把 pt 单位转为 px 单位,需将 pt 值乘以 1.333(因为 1 英寸为 96px,并且 1 英寸也等于 72pt,因此 96 / 72 = 1.333px/pt)。因此 18pt × 1.333px/pt = 24px。然后除以字号,就得到了不带单位的行高值,即 24px / 16px = 1.5

而字距(tracking)通常会给定某个数字,例如 100。因为一个单位的该数字表示 1em 的千分之一,因此除以 1000 就可以转为 em 单位值,即 100 / 1000 = 0.1em

12.6.2 标题、小元素和间距

Headings, small elements, and spacing

标题的间距通常和正文内容不太一样。正文间距调整好后,需要检查一下标题,看看是否也需要调整。

标题一般比较简短,通常只有几个字,但即便偶尔才会遇到长标题,页面样式也应该考虑这种情况。在页面设计时常犯的一个错误就是只测试短标题。既然页面行高已经设定,就可以试着给各级标题添加文字内容,看看标题强制换行后的效果如何(如图 12.18)。

图 12.18 让标题强制换行,看看行高是否合适
图 12.18 让标题强制换行,看看行高是否合适

在本例中,由于垂直间距看上去还可以,这里就不做修改了。但检查行高这一步绝不能少。有时候 1.4 倍行高可能会显得有点宽,这也要看所选用的字型(typeface),尤其是设置大字号的时候。我曾经遇到过一些网站就是这样的情况,最后不得不把标题的行高调低到 1.0

而对于正文主体而言,调整间距的重点在于使用户的阅读体验效果最佳。但对于标题以及其他内容偏少的页面元素(如按钮)来讲,这一点影响不大。这时字间距的可调节范围就大大增加了,也可以有更多发挥想象的空间了,甚至可以使用负的字间距来让文字渲染得更加紧凑。例如图 12.19 里的标语就声明了 letter-spacing: -0.02em 的样式。

图 12.19 页面上内容简短、风格鲜明的部分,可以考虑使用更为紧凑的字符间距
图 12.19 页面上内容简短、风格鲜明的部分,可以考虑使用更为紧凑的字符间距

上述样式的间距变化还是很明显的(dramatic)。如果是几段文件都用这样的间距样式,阅读起来就会很费劲;但对于小段文本效果还不错(也就几个字)。于是标题就按这个版本设置,并根据代码清单 12.13 同步更新本地样式表。

代码清单 12.13 紧凑版标语的字间距设置

.hero h2 {
  margin-block: 0 10px;
  font-size: 1.95rem;
  letter-spacing: -0.02em; /* 利用负的 letter-spacing 来压缩字间距 */
}

我们也可以重新评估一下页面小型元素的间距和文本,例如按钮。在我看来此时的按钮看起来稍微偏大了些,尤其是页头的导航按钮部分。我们来调整一下。图 12.20 展示了现在的效果(上)以及调整后的效果(下)。

图 12.20 调整文本属性可以改善导航按钮的外观

【图 12.20 调整文本属性可以改善导航按钮的外观】

这里做了如下调整:减小字号,使用 text-transform 把字母转为大写,并上调字符间距(letter spacing)。

提示
通常字母全部大写的文字再配合较大的字间距,看上去效果会更好一些。

要实现上述效果,请将代码清单 12.14 中带注解内容的样式声明同步到本地样式表中。

代码清单 12.14 调整导航菜单项上的尺寸和间距样式

.nav-container__inner {
  display: flex;
  justify-content: space-between;
  align-items: end; /* 令导航容器中的元素底部对齐 */
  max-inline-size: 1080px;
  margin-inline: auto;
  padding: 0.625em 0;
}

...

.top-nav a {
  display: block;
  font-size: 0.8rem; /* 减小导航链接和按钮的字号 */
  padding: 0.3rem 1.25rem; /* 将内边距的值由 em 改为 rem */
  color: var(--white);
  background: var(--brand-green);
  text-decoration: none;
  border-radius: 3px;
  text-transform: uppercase; /* 将导航链接改为大写 */
  letter-spacing: 0.03em; /* 增加字间距 */
}

因为调小了导航链接的字号,所以它们将不再填充 nav-container 内容盒的高度。默认情况下这些链接元素是顶部对齐的,下方会空出一些区域。将 nav-container 的弹性子元素设为底部对齐(即 align-items: end)就可以解决这个问题。

由于导航元素的字号已经改变,其内边距(之前以相对单位 em 来设置大小)也会随之改变。为此,这里将尺寸单位改为 rem。当然也可以通过计算得出新的以 em 为单位的相对尺寸,但这样做并不值得。

text-transform 属性可能较为陌生。它可以把所有字母改为大写,无论在 HTML 中是如何书写的。这里强烈推荐这种方式,而不是到 HTML 里手动将文字改为大写。这样依赖,如果将来设计稿修改了,就可以只改一行 CSS,而不必在所有 HTML 页面的多个位置进行修改。只有当需要遵循某种语法规则的大写(例如首字母缩略词)时,才应该在 HTML 中大写。而像本例这样只是单纯出于设计上的考虑而渲染的大写形式,仅通过 CSS 就能实现。

text-transform 属性的另一个合法值为 lowercase,用于将所有字母转为小写。此外还可以设为 capitalize,用于将每个单词的首字母转为大写形式、其余字母保持 HTML 中的原始写法。

12.7 本章小结 Summary


  1. 译注:若采用外边距实现起来会比较繁琐。上一版就是用的左外边距,需要和猫头鹰选择器搭配使用,写作:.top-nav > li + li { margin-left: 0.625em; }↩︎