在 CSS 中,line-height
可能是最容易被误解,但最常用的属性之一。作为设计师和开发者,当我们想到 line-height
时,我们可能会想到 印刷设计中的行距 的概念——有趣的是,这个词源于将铅块放在铅字行的字间距中。
行距和 line-height
虽然很相似,但也有一些重要的区别。为了理解这些差异,我们首先需要更多地了解一下排版。
排版术语概述
在传统的西方字体设计中,一行文字由几个部分组成:
- 基线:这是字体所在的假想线。当你用有格子的笔记本写字时,基线就是你写字的那条线。
- 下沉线:这条线位于基线下方。这是某些字符——如小写
g
、j
、q
、y
和p
——触及基线以下的线。 - X 高度:这是(不出所料)一行文字中正常的小写
x
的高度。通常,这是其他小写字母的高度,虽然有些字母的字符部分可能会超过 x 高度。就所有意图和目的而言,它作为小写字母的感知高度。 - 大写高度:这是给定一行文字中大多数大写字母的高度。
- 上伸线:一条线,通常出现在大写高度之上,在那里一些字符,比如小写
h
或b
,可能会超过正常的大写高度。

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

一个 CSS 开发人员可能会想,“好吧,行距就是 line-height
,我们可以继续了。” 虽然两者相关,但它们在一些非常重要的方面也有所不同。
让我们拿一个空白文档,并在其中添加一个经典的 “CSS 重置”
* {
margin: 0;
padding: 0;
}
这会删除每个元素的边距和内边距。
我们还将使用 来自 Google Fonts 的 Lato 作为我们的 font-family
。
我们需要一些内容,所以让我们创建一个 <h1>
标签,其中包含一些文本,并将 line-height
设置为一个令人讨厌的巨大值,比如 300px。结果是一行文字,在该行文字之上和之下都有大量空格。
当浏览器遇到 line-height
属性时,它实际上会做的是将该行文字放在一个“行框”的中间,该行框的高度与元素的行高相匹配。我们没有设置字体的行距,而是得到了类似于在行框的每一侧添加内边距的效果。

如上图所示,行框围绕一行文字,行距是通过在一行文字下方和下一行文字上方使用空格来创建的。这意味着,页面上的每个文本元素都将有一半的行距位于第一个文本行的上方和一个特定文本块中的最后一个文本行的下方。
更令人惊讶的是,在具有相同值的元素上显式设置 line-height
和 font-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-trim
或 leading-trim
的新属性到文本元素。
网络语言的妙处之一在于,我们都可以参与其中。如果您认为这项功能应该成为 CSS 的一部分,您可以在该线程中添加评论,表达您的想法。
非常有趣的技巧!包含元素将需要顶部/底部填充以确保文本不会溢出。
您是否知道任何缺点?例如,对文本密集页面应用额外样式带来的性能下降?
有趣的方法,您知道调整会对性能造成多少影响吗?我觉得这会增加布局时间?
我尝试以更简单的方式处理行高,只是确保垂直节奏一致
https://medium.com/creative-technology-concepts-code/css-vertical-rhythm-with-typography-units-e2c3482a4ac0
我一开始并不确定这篇文章想要表达什么,感觉有点无聊。
但我对网络排版的兴趣让我坚持读了下去...
天啊!感谢上帝,我坚持读完了!
这简直让我大开眼界!太棒了!
未来将会是使用 JavaScript 和 Sass 引擎的 Illustrator(tm) 来设计网页!
我的天!
这篇文章非常详细,看得出你下了很多功夫。
我建议你重构 mixin,使用无单位值表示行高,并使用相对单位表示字体大小。你可以通过使用相同的 mixin 逻辑设置比率来实现相同的像素网格锁定。
之所以建议这样做是因为绝对值,尤其是行高,对于那些放大或以其他方式修改字体大小以方便阅读的人来说可能存在问题。通过在输出 CSS 中保持比例,你可以确保更广泛的浏览模式和上下文支持。
如果有人感兴趣,这里有一个在基线网格上对齐类型的原生 CSS 方法
在 mixin 中:
padding-top: $preventCollapse;
在结果中:
padding-top: 1;
— 无效的属性值 )这个规则有必要吗?当这个规则应用后,标题之间会出现一个像素寄生虫 :(
很棒的文章!谢谢,我一定会把它用在我的当前和未来的项目中。它与 Codyhouse 的行高裁剪 (https://codyhouse.co/ds/globals/typography) 和 Eightshapes 文本裁剪工具 (http://text-crop.eightshapes.com/?typeface-selection=custom-font&typeface=Lato&custom-typeface-name=IBM%20Plex%20Serif&custom-typeface-url=http%3A%2F%2Fwww.paulrand.design%2Fhome%2Fdlewand691%2Fwww.paulrand.design%2Fnode_modules%2F%40ibm%2Fplex%2FIBM-Plex-Serif%2Ffonts%2Fsplit%2Fwoff2%2FIBMPlexSerif-Regular-Latin1.woff2&custom-typeface-weight=400&custom-typeface-style=normal&weight-and-style=100&size=51&line-height=1.2&top-crop=13&bottom-crop=12) 非常相似。
谢谢 Daniel!如果你想了解更多关于我的方法,可以查看这里: https://medium.com/eightshapes-llc/cropping-away-negative-impacts-of-line-height-84d744e016ce
它不会像设计工具一样工作,它只是从一段文本中移除顶部和底部的空间。我认为与 basekick 的目标略有不同。
这是一种很棒的方法,我第一次看到它是在 Mark 的推特帐户上 :D
我有一个问题:有可能在 Figma 中复制这种方法吗?
据我所知,Figma 的行高行为与网络1 相似;因此在 Figma 中这样做就像一个大的 hack,这违背了这种方法的初衷。
如果有人正在使用这种工作流程,我希望得到一些关于如何做同样事情的提示 :)
嘿 Palash!
我也一直在研究这个问题,并且得出了同样的结论。
这里有一个解决方法,但它并不完美。解决方案在这里有很好的记录: https://uxdesign.cc/the-4px-baseline-grid-89485012dea6,Figma 也对这方面有一些介绍: https://www.figma.com/best-practices/everything-you-need-to-know-about-layout-grids/baseline-grids/
不过我开始意识到,最终,如果你必须为 CSS 和 Figma 添加一个解决方法才能让基线网格正常工作,那么你会质疑追求基线网格是否值得 lol。
我一直在做的是在 Figma 中设置垂直网格以与行框配合使用,并相应地调整其他元素的间距。在 CSS 原生支持之前,我认为 Figma 不会改变它的行高属性。
虽然有趣,但这完全没有用。排版之所以留出上下间隙是有原因的。虽然在字符数量有限的语言中可能没有用处,但有些语言使用带有重音和其他附加符号的字符,比如法语:ÊÉÈ 或 Ç。在我的母语中,我们使用像 ĚŠČŘŽÝÁÍÉ… 哦,有些甚至不受字体支持。你看到 S 上面的那个小东西,用来构成 Š 吗?好吧,那个重音也应该出现在 Ě、Č 和 Ř 上面。
它没有涵盖大写字母中重音的使用情况,这并不能让它毫无用处 :) 它可以使用修复,当然。也许只需要增加 $capHeight?
我在这里写了 Megatype。几年前我们遇到了类似的问题:从基线到下一行的大写高度测量排版间距。我注意到一个主要区别是,在你指定一个降部大小的地方,我们只是使用了一个 y 偏移量来在 y 轴上移动带有长降部的字体。我认为你的方法更复杂。
然而,据我记忆,有一个问题让我们止步不前,那就是我们发现,比你想象的更常见的是,有些字体设置的度量不佳。这意味着不幸的是,它们在 Windows 上的渲染效果可能与 Mac 上不同,因为它们有时会查看完全不同的度量,而且很难看到这是否可以通过 CSS 解决。对我们来说,问题是看到一个网站在 Mac 上渲染得很好,但在 Windows 上,一种字体向下移动了几像素,而另一种向上移动了几像素。真是太可惜了...
我最终得出一个令人沮丧的结论:它无法在 CSS 中*可靠地*实现。没有 JS 不行,没有更好的原生 CSS 排版属性也不行(看看 ch 和 ex 单位 — 还有我参与贡献的原生大写高度单位提案)。但这已经是几年前的事了,我绝不是专家。一旦字体度量表需要成为解决方案的一部分,就会变得呈指数级地更加复杂。
更多关于我们当时在做的事情的信息:
https://medium.com/@tbredin/a-jolly-web-typesetting-adventure-42948ab0d1dd
我认为行距的定义是错误的。它是行与行之间增加的距离,也就是降部与升部之间的距离。在手工排版中,这是通过在每行之间添加一条铅条来实现的。
来自维基百科,“在手工排版中,行距是指插入在排版尺中类型行之间的薄铅条,用于增加它们之间的垂直距离。铅条的厚度称为行距,它等于类型尺寸与两个基线之间的距离之差。例如,给定一个 10 点的类型大小和两个基线之间的 12 点的距离,行距将是 2 点。”
很棒的文章,我建议加入一个用于计算降部和升部高度的视觉效果。
再次回顾这项技术。对于图像等不同的元素,该如何调整才能始终与基线对齐?
字体系列定义通常包含几种字体和一个通用字体名称作为最后一个选项,例如
当使用的字体具有不同的度量时,如果客户端没有使用列表中的第一个字体,则此技术的值可能会失败。
我认为基于字体固有度量值的 CSS 属性会很不错,例如根据基线设置高度。或者在使用 `text-justify:inter-character` 时修剪空像素。
愿上帝保佑你。
关于所有这些的一个后续问题:如何处理在尝试对齐 2 行或多行文本时经常遇到的水平对齐问题。
上面的示例之一完美地说明了这一点。此处为屏幕截图:https://imgur.com/a/mt5jHEI
我们如何确保第一行的第一个字符始终与第二行的第一个字符对齐,无论它们分别以什么字母开头?
在我看来,唯一的方法是使用边距或平移偏移,这些偏移需要逐字符确定。换句话说,你可能会为“H”将第一行偏移 -3 像素或其他值,但对于“D”可能就不好使了。
我讲清楚了吗?请有人帮忙!我在这儿陷入了排版地狱。谢谢!