如何在 CSS 中驯服行高

Avatar of Caleb Williams
Caleb Williams

DigitalOcean 提供适用于旅程各个阶段的云产品。立即开始使用 $200 免费信用额度!

在 CSS 中,line-height 可能是最容易被误解,但最常用的属性之一。作为设计师和开发者,当我们想到 line-height 时,我们可能会想到 印刷设计中的行距 的概念——有趣的是,这个词源于将铅块放在铅字行的字间距中。

行距和 line-height 虽然很相似,但也有一些重要的区别。为了理解这些差异,我们首先需要更多地了解一下排版。

排版术语概述

在传统的西方字体设计中,一行文字由几个部分组成:

  • 基线:这是字体所在的假想线。当你用有格子的笔记本写字时,基线就是你写字的那条线。
  • 下沉线:这条线位于基线下方。这是某些字符——如小写 gjqyp——触及基线以下的线。
  • X 高度:这是(不出所料)一行文字中正常的小写 x 的高度。通常,这是其他小写字母的高度,虽然有些字母的字符部分可能会超过 x 高度。就所有意图和目的而言,它作为小写字母的感知高度。
  • 大写高度:这是给定一行文字中大多数大写字母的高度。
  • 上伸线:一条线,通常出现在大写高度之上,在那里一些字符,比如小写 hb,可能会超过正常的大写高度。
Illustrating the ascender, cap height, x-height, baseline and descender of the Lato font with The quick fox as sample text.

上面描述的每个文本部分都是字体本身固有的。字体是在考虑了所有这些部分的情况下设计的;然而,排版中有些部分是由排版者(比如你和我的)而不是设计师决定的。其中之一就是行距。

行距定义为一组字体中两条基线之间的距离。

Two lines of text with an order box around the second line ofd text indicating the leading.

一个 CSS 开发人员可能会想,“好吧,行距就是 line-height,我们可以继续了。” 虽然两者相关,但它们在一些非常重要的方面也有所不同。

让我们拿一个空白文档,并在其中添加一个经典的 “CSS 重置”

* {
  margin: 0;
  padding: 0;
}

这会删除每个元素的边距和内边距。

我们还将使用 来自 Google Fonts 的 Lato 作为我们的 font-family

我们需要一些内容,所以让我们创建一个 <h1> 标签,其中包含一些文本,并将 line-height 设置为一个令人讨厌的巨大值,比如 300px。结果是一行文字,在该行文字之上和之下都有大量空格。

当浏览器遇到 line-height 属性时,它实际上会做的是将该行文字放在一个“行框”的中间,该行框的高度与元素的行高相匹配。我们没有设置字体的行距,而是得到了类似于在行框的每一侧添加内边距的效果。

Two lines of text with orange borders around each line of text, indicating the line box for each line. The bottom border of the first line and the top border of the second line are touching.

如上图所示,行框围绕一行文字,行距是通过在一行文字下方和下一行文字上方使用空格来创建的。这意味着,页面上的每个文本元素都将有一半的行距位于第一个文本行的上方和一个特定文本块中的最后一个文本行的下方。

更令人惊讶的是,在具有相同值的元素上显式设置 line-heightfont-size 会在文本之上和之下留下额外的空间。我们可以通过向元素添加背景颜色来观察这一点。

这是因为,即使 font-size 设置为 32px,但实际的文本大小由于生成的间距而小于该值。

让 CSS 像处理行距一样处理行高

如果我们希望 CSS 使用更传统的排版风格而不是行框,我们希望一行文字在其上方或下方没有任何空格——但允许多行元素保持其完整的 line-height 值。

通过付出一些努力,可以让 CSS 学习行距。Michael Taranto 发布了一个名为 Basekick 的工具,它解决了这个问题。它是通过对 ::before 伪元素应用负上边距,以及对元素本身应用 translateY 来实现的。最终的结果是一行文字,没有在其周围添加任何额外的空格。

Basekick 公式的最新版本可以在 SEEK 的 Braid Design System 的源代码 中找到。在下面的示例中,我们编写了一个 Sass 混合器来为我们完成繁重的工作,但相同的公式可以与 JavaScript、Less、PostCSS 混合器或任何提供这些数学功能的其他工具一起使用。

@function calculateTypeOffset($lh, $fontSize, $descenderHeightScale) {
  $lineHeightScale: $lh / $fontSize;
  @return ($lineHeightScale - 1) / 2 + $descenderHeightScale;
}


@mixin basekick($typeSizeModifier, $baseFontSize, $descenderHeightScale, $typeRowSpan, $gridRowHeight, $capHeight) {
  $fontSize: $typeSizeModifier * $baseFontSize;
  $lineHeight: $typeRowSpan * $gridRowHeight;
  $typeOffset: calculateTypeOffset($lineHeight, $fontSize, $descenderHeightScale);
  $topSpace: $lineHeight - $capHeight * $fontSize;
  $heightCorrection: 0;
  
  @if $topSpace > $gridRowHeight {
    $heightCorrection: $topSpace - ($topSpace % $gridRowHeight);
  }
  
  $preventCollapse: 1;
  
  font-size: #{$fontSize}px;
  line-height: #{$lineHeight}px;
  transform: translateY(#{$typeOffset}em);
  padding-top: $preventCollapse;


  &::before {
    content: "";
    margin-top: #{-($heightCorrection + $preventCollapse)}px;
    display: block;
    height: 0;
  }
}

乍一看,这段代码肯定看起来像是很多拼凑在一起的魔术数字。但是,我们可以通过将其放在特定系统的上下文中来对其进行相当程度的分解。让我们看看我们需要了解哪些内容

  • $baseFontSize:这是我们系统的正常 font-size,所有其他内容都将围绕它进行管理。我们将使用 16px 作为默认值。
  • $typeSizeModifier:这是一个乘数,与基本字体大小一起使用来确定 font-size 规则。例如,一个值为 2 的乘数与我们的基本字体大小 16px 相结合,将得到 font-size: 32px
  • $descenderHeightScale:这是字体的下沉线高度,以比率表示。对于 Lato,这似乎大约是 0.11。
  • $capHeight:这是字体的特定大写高度,以比率表示。对于 Lato,这大约是 0.75。
  • $gridRowHeight:布局通常依赖于默认的垂直节奏,以创造美观且间距一致的阅读体验。例如,页面上的所有元素可能以 4 或 5 像素的倍数间隔开。我们将使用 4 作为值,因为它可以很容易地被我们的 $baseFontSize 16px 除尽。
  • $typeRowSpan:与 $typeSizeModifier 一样,此变量充当乘数,与网格行高一起使用来确定规则的 line-height 值。如果我们的默认网格行高为 4,我们的类型行跨度为 8,那么我们将得到 line-height: 32px。

现在,我们可以将这些数字代入上面的 Basekick 公式(借助 SCSS 函数和混合器),这将为我们提供以下结果。

这正是我们想要的。对于任何没有边距的文本块元素集,这两个元素应该相互碰撞。这样,在两个元素之间设置的任何边距都将是像素完美的,因为它们不会与行框间距冲突。

优化我们的代码

与其将所有代码都倾倒在一个 SCSS 混合器中,不如将其组织得更好。如果我们从系统的角度考虑,我们会注意到我们正在处理三种类型的变量

变量类型描述混合器变量
系统级这些值是我们正在使用的设计系统的属性。$baseFontSize
$gridRowHeight
字体级这些值是我们在使用的字体的固有值。可能需要一些猜测和调整才能获得完美的数字。$descenderHeightScale
$capHeight
规则级这些值是针对我们创建的 CSS 规则的。$typeSizeMultiplier
$typeRowSpan

从这些方面考虑问题将帮助我们更轻松地扩展我们的系统。让我们依次考虑每个组。

首先,系统级变量可以全局设置,因为这些变量在我们项目进行过程中不太可能发生变化。这将我们主要混合器中的变量数量减少到四个

$baseFontSize: 16;
$gridRowHeight: 4;

@mixin basekick($typeSizeModifier, $typeRowSpan, $descenderHeightScale, $capHeight) {
  /* Same as above */
}

我们还知道字体级变量是特定于其给定字体家族的。这意味着创建更高阶的混合器来将它们设置为常量会很容易

@mixin Lato($typeSizeModifier, $typeRowSpan) {
  $latoDescenderHeightScale: 0.11;
  $latoCapHeight: 0.75;
  
  @include basekick($typeSizeModifier, $typeRowSpan, $latoDescenderHeightScale, $latoCapHeight);
  font-family: Lato;
}

现在,在规则基础上,我们可以轻松地调用 Lato 混合器

.heading--medium {
  @include Lato(2, 10);
}

该输出将为我们提供一个规则,该规则使用 Lato 字体,font-size 为 32px,line-height 为 40px,以及所有相关的转换和边距。这允许我们编写简单的样式规则,并利用设计师在使用 Sketch 和 Figma 等工具时习惯的网格一致性。

因此,我们可以轻松地创建像素完美的 desain,而且毫不费力。请查看下面的示例是如何完美地与我们的基本 4px 网格对齐的。(你可能需要放大才能看到网格。)

这样做赋予我们在创建网站布局时独一无二的超能力:我们可以在历史上第一次真正创建像素完美的页面。将这种技术与一些基本布局组件结合起来,我们就可以像在设计工具中一样开始创建页面。

走向标准化

虽然让 CSS 的行为更像我们的设计工具需要一些努力,但未来可能会有好消息。一个 针对 CSS 规范的补充提案 旨在原生实现这种行为切换。目前提案中,将添加一个类似于 line-height-trimleading-trim 的新属性到文本元素。

网络语言的妙处之一在于,我们都可以参与其中。如果您认为这项功能应该成为 CSS 的一部分,您可以在该线程中添加评论,表达您的想法。