深入理解 CSS line-height 和 vertical-align的用法

來源:互聯網
上載者:User
line-height 和 vertical-align 都是簡單的 CSS 屬性,以致於大多數人自以為知道這兩個屬性的工作原理。但實際上這兩個屬性非常複雜,也許算得上是 CSS 裡最難的兩個屬性,因為這兩個屬性和 CSS 裡一個鮮為人知的特性息息相關:內聯格式化上下文(IFC),本文就和大家分享 CSS中 line-height 和 vertical-align的用法,希望能協助到大家。

舉例來說,line-height 的值可以是一個長度(length)或者是一個數字,它的預設值是 normal。那麼,normal 是什麼呢?我們經常將 normal 理解為 1,或者 1.2,甚至連 CSS 規格文檔都沒有提到這一問題。我們知道 line-height 的值為數字時,表示的相對於 font-size 的倍數,但問題在於,font-size:100px 對應的文字在不同字型裡的高度是不一樣的!那麼 line-height 會隨著文字大小的改變而改變嗎? normal 真的表示 1 或者 1.2 嗎?vertical-align 又是如何被 line-height 影響的呢?

讓我們來深入理解一個不那麼簡單的 CSS 機制。

font-size

下面是一段簡單的 HTML 程式碼,一個 p 標籤包含了 3 個 span 標籤,每個 span 各自有一個 font-family:

<p>    <span class="a">Ba</span>    <span class="b">Ba</span>    <span class="c">Ba</span></p>
 p  { font-size: 100px }.a { font-family: Helvetica }.b { font-family: Gruppo    }.c { font-family: Catamaran }

(譯者註:這幾款字型你的電腦上可能沒有)

font-size 相同,font-family 不同,得到的 span 元素的高度也不同:

為什麼 font-size: 100px 不能得到相同高度的元素呢?我測量了一下每個 span 的高度:Helvetica 115px,Gruppo 97px,Catamaran 164px。

乍看很奇怪,但是仔細想想,這麼做又是很有道理的。原因在於字型本身,這是字型的原理:

  • 一款字型會定義一個 em-square,它是用來盛放字元的金屬容器。這個 em-square 一般被設定為寬高均為 1000 相對單位,不過也可以是 1024、2048 相對單位。

  • 字型度量都是基於這個相對單位設定的,包括 ascender、descender、capital height、x-height 等。注意這裡面的值是允許相對於 em-square 出血(bleed outside)的(譯者註:大概可以理解為超出 em-square)

  • 在瀏覽器中,上面的 1000 相對單位會按照你需要的 font-size 縮放。

我們把 Catamaran 字型放到 FontForge 中,分析它的字型度量:

  • em-square 是 1000

  • ascender 是 1100,descender 是 540。通過測試發現,macOS 上的瀏覽器使用了 HHead Ascent 和 HHead Descent 值,Windows 上的瀏覽器使用了 Win Ascent 和 Win Descent(而且兩個平台上的值不一樣)。我們還看到 Capital Height 是 680,X height 是 485。

這意味著 Catamaran 字型佔據了 1100 + 540 個相對單位,儘管它的 em-square 只有 1000 個相對單位,所以當我們設定 font-size:100px 時,這個字型裡的文字高度是 164px。這個計算出來的高度決定了 HTML 元素的 content-area(內容地區),後面我會講到 content-area。你可以認為 content-area 就是 background 作用的地區。

我們還能看出大寫字母的高度是 68px,小寫字母的高度(x-height)是 49px。所以 1ex = 49px,1em = 100px,而不是 164px。(真好,em 是基於 font-size,而不是基於計算出來的高度)

在繼續深入之前,說點相關的知識。當 p 元素出現在螢幕上時,它可能包含了多行內容,每行內容由多個內嵌元素組成(內聯標籤或者是包含文本的匿名內嵌元素),每一行都叫做一個 line-box。line-box 的高度是由它所有子項目的高度計算得出的。瀏覽器會計算這一行裡每個子項目的高度,再得出 line-box的高度(具體來說就是從子項目的最高點到最低點的高度),所以預設情況下,一個 line-box 總是有足夠的高度來容納它的子項目。

每個 HTML 元素實際上都是由多個 line-box 的容器,如果你知道每個 line-box 的高度,那麼你就知道了整個元素的高度。

如果我們修改一下最初的 HTML 程式碼:

<p>    Good design will be better.    <span class="a">Ba</span>    <span class="b">Ba</span>    <span class="c">Ba</span>    We get to make a consequence.</p>

那麼就會得到 3 個 line-box(寬度固定):

  • 第一行和最後一行各有一個匿名內嵌元素(常值內容)

  • 中間一行包含兩個匿名內嵌元素和三個 span

我們清楚地看到第二個 line-box比其他兩個要高一些。因為第二行裡面的子項目因為有一個用到了 Catamaran 字型的 span。

line-box 的痛點在於我們看不見它,而且不能用 CSS 控制它。即使我們用 ::first-line 給第一行加上背景色,我們也看不出第一個 line-box 的高度。

line-height

目前我已經提到了兩個概念:content-area 和 line-box。如果你仔細看了,會發現我說 line-box 的高度是根據子項目的高度計算出來的,而不是子項目的 content-area 的高度。這個區別大了。
接下來說句聽起來很奇怪的話:一個內嵌元素有兩個高度:content-area 高度和 virtual-area (實際地區?)高度(virtual-area 是我自己發明的單詞,它表示對人類有效高度,你在其他地方是看不到這個單詞的)。

  • content-area 的高度是由字型度量定義的(見上文)

  • vitual-area 的高度就是 line-height,這個高度用於計算 line-box 的高度

這麼一來,這就打破了一個長久的謠言:line-height 表示兩個 baseline 之間的距離。在 CSS 裡,不是這樣的。

virtual-area 和 content-area 高度的差異叫做 leading。leading 的一半會被加到 content-area 頂部,另一半會被加到底部。因此 content-area 總是處於 virtual-area 的中間。

計算出來的 line-height(也就是 virtual-area 的高度)可以等於、大於或小於 content-area。如果 virtual-area 小於 content-area,那麼 leading 就是負的,因此 line-box 看起來就比內容還矮了。

還有一些其他種類的內嵌元素:

  • 可替換的內嵌元素,如 img / input / svg 等

  • inline-block 元素,以及所有 display 值以 inline- 開頭的元素,如 inline-table / inline-flex

  • 處於某種特殊格式化內容相關的內嵌元素,例如 flexbox 元素中的子項目都處於 flex formatting context(彈性格式化上下文)中,這些子項目的 display 值都是「blockified」

這類內嵌元素,其高度是基於 height、margin 和 border 屬性(譯者註:好像漏了 padding)。如果你將其 height 設定為 auto 的話,那麼其高度的取值就是 line-height,其 content-area 的取值也是 line-height。

我們目前依然沒有解釋 line-height:normal 是什麼意思。要解答這個問題,我們又得回到 content-area 高度的計算了,問題的答案就在字型度量裡面。

我們回到 FontForge,Catamaran 的 em-square 高度是 1000,同時我們還看到很多其他的 ascender/descender 值:

  • 常規的 Ascent/Descent:ascender 是 770,descender 是 230,用於渲染字元。

  • 規格 Ascent/Descent:ascender 是 1100,descender 是 540。用於計算 content-area 的高度

  • 規格 Line Gap:用於計算 line-height: normal。

在 Catamaran 這款字型中,Line Gap 的值是 0,那麼 line-height: normal 的結果就跟 content-area 的高度一樣,是 1640 相對單位。

為了對比,我們再看看 Arial 字型,它的 em-square 是 2048,ascender 是 1854,descender 是 434,line gap 是 67。那麼當 font-size: 100px 時,

  • 其 content-area 的高度就是 100/2048*(1854+434) = 111.72,約為 112px;

  • 其 line-height: normal 的結果就是 100/2048*(67+1854+434) 約為 115px。

所有這些值都是由字型設計師設定的。

這麼看來,line-height:1 就是一個很糟糕的實踐。記得嗎,當 line-height 的值是一個數字時,其實就是相對 font-size 的倍數,而不是相對於 content-area。所以 line-height:1 很有可能使得 virtual-area 比 content-area 矮,從而引發很多其他的問題。

不僅僅是 line-height:1 有問題,我電腦上的 1117 款字型中,大概有 1059 款字型的 line-height 比 1 大,最低的是 0.618,最高的是 3.378。你沒看錯,是 3.378!

line-box 計算的一些細節:

  • 對於內嵌元素,padding 和 border 會增大 background 地區,但是不會增大 content-area(不是 line-box 的高度)。一般來說你無法再螢幕上看到 content-area。margin-top 和 margin-bottom 對兩者都沒有影響。

  • 對於可替換內嵌元素(replaced inline elements)、inline-block 元素和 blockified 內嵌元素,padding、margin 和 border 會增大 height(譯者註:注意 margin),因此會影響 content-area 和 line-box 的高度

vertical-align

我還沒提過 vertical-align 屬性,它也是計算 line-box 高度的重要因素之一。我們甚至可以說 vertical-align 是內聯格式化上下文(IFC)中最重要的屬性。

它的預設值是 baseline。還記得字型度量裡的 ascender 和 descender 嗎?這兩個值決定了 baseline 的位置。很少有

字型的 ascender 和 descender 的比例是一比一的,所以我們經常看到一些意想不到的現象,下面是例子。

代碼如下:

<p>    <span>Ba</span>    <span>Ba</span></p>
p {    font-family: Catamaran;    font-size: 100px;    line-height: 200px;}

一個 p 標籤內有兩個 span 標籤,span 繼承了 font-family、font-size 和 200px 的 line-height。這時兩個 span 的 baseline 是等高的,line-box 的高度就是 span 的 line-height。

如果第二個 span 的 font-size 變小了呢?

span:last-child {    font-size: 50px;}

我們會發現一個非常奇怪的現象,line-box 的高度變高了!如所示。提示你一下,line-box 的高度是從子項目的最高點到最低點的舉例。

這個例子可以作為「應該將 line-height 的值寫成數字」的論據,但是有時候我們為了做出好看的排版,必須把 line-height 寫成一個固定值。

不過我實話告訴你吧,不管你把 line-height

寫成什麼,你都會在對齊內嵌元素的時候遇到麻煩。
我們來看另一個例子。p 標籤有 line-height:200px,內含一個 span,span 繼承了 p 的 line-height。

<p>    <span>Ba</span></p>
p {    line-height: 200px;}span {    font-family: Catamaran;    font-size: 100px;}

此時 line-box 的高度是多少?貌似是 200px,但其實不是。這裡你沒有考慮到的問題是 p 有自己的 font-family,預設值是 serif。p 的 baseline 和 span 的 baseline 位置不一樣,因此最終的 line-box 比我們預想的要高一些。出現這種問題是因為瀏覽器認為每個 line-box 的起始位置都有一個寬度為 0 的字元(CSS 文檔將其稱為 strut),並將其納入 line-box 的高度的計算中。

看不見的字元,看得見的影響。

為了說明這個問題,我們畫圖解釋一下這個問題。

用 baseline 來對齊令人費解,如果我們用 vertical-align: middle 會不會好一點呢?讀 CSS 文檔你會發現,middle 的意思是「用父元素 baseline 高度加上父元素中 x-height 的一半的高度來對齊當前元素的垂直方向的中點」。baseline 所處的高度跟字型有關,x-height 的高度也跟字型有關,所以 middle 對齊也不靠譜。更糟糕的是,一般來說,middle 根本就不是置中對齊!內嵌元素的對齊受太多因素影響,因此不可能用 CSS 實現。

順便一說,vertical-align 的其他 4 個值有可能有點用:

  • vertical-align: top / bottom,表示與 line-box 的頂部或底部對齊

  • vertical-align: text-top / text-bottom,表示與 content-area 的頂部或底部對齊

不過你依然要小心,大部分情況下,對齊的是 virtual-area,也就是一個不可見的高度。看看下面這個用 vertical-align:top 的例子:

最後,vertical-align 的值也可以是數字,表示根據 baseline 升高或降低,不到萬不得已還是別用數字吧。

CSS is awesome

我們討論了 line-height 和 vertical-align 如果互相影響,現在問題來了:CSS 可以控制字型度量嗎?簡單來說答案是:不行。我也很想用 CSS 來控制字型。無論怎樣,我還是想試試。字型度量只是一些固定的值而已,我們應該可以圍繞它做點什麼。

比如說,我們想要一段文字使用 Catamaran 字型,同時大寫字母的高度正好是 100px,看起來可以實現,我們只需要一些數學知識。

首先我們把所有字型度量設定為 CSS 自訂屬性,然後計算出一個 font-size,讓大寫字母的高度正好是 100px。

p {    /* font metrics */    --font: Catamaran;    --fm-capitalHeight: 0.68;    --fm-descender: 0.54;    --fm-ascender: 1.1;    --fm-linegap: 0;    /* desired font-size for capital height */    --capital-height: 100;    /* apply font-family */    font-family: var(--font);    /* compute font-size to get capital height equal desired font-size */    --computedFontSize: (var(--capital-height) / var(--fm-capitalHeight));    font-size: calc(var(--computedFontSize) * 1px);}

看起來也並不複雜不是嗎?如果我們想要文字垂直置中怎麼辦呢?也就是讓 B 上面的空間和下面的空間高度一樣。為了做到這一點,我們必須要根據 ascender 和 descender 的比例來計算 vertical-align。

首先計算出 line-height:normal 的值和 content-area 的高度:

p {    …    --lineheightNormal: (var(--fm-ascender) + var(--fm-descender) + var(--fm-linegap));    --contentArea: (var(--lineheightNormal) * var(--computedFontSize));}

然後我們需要計算:

  • B 下面空間的高度

  • B 上面空間的高度

像這樣:

p {    …    --distanceBottom: (var(--fm-descender));    --distanceTop: (var(--fm-ascender) - var(--fm-capitalHeight));}

然後我們就可以計算 vertical-align 的值。

p {    …    --valign: ((var(--distanceBottom) - var(--distanceTop)) * var(--computedFontSize));}span {    vertical-align: calc(var(--valign) * -1px);}

最後,設定 line-height:

p {    …    /* desired line-height */    --line-height: 3;    line-height: calc(((var(--line-height) * var(--capital-height)) - var(--valign)) * 1px);}

添加一個和 B 一樣高的 icon 就很容易了:

span::before {    content: '';    display: inline-block;    width: calc(1px * var(--capital-height));    height: calc(1px * var(--capital-height));    margin-right: 10px;    background: url('https://cdn.pbrd.co/images/yBAKn5bbv.png');    background-size: cover;}

JSBin 效果示範

注意這隻是為了示範,請不要在生產環境中使用此方案。

結論

我們知道了:

  • IFC 真的很難懂

  • 所有的內嵌元素都有兩個高度

    • 基於字型度量的 content-area

    • virtual-area(也就是 line-height )

    • 這兩個高度你都無法看到

  • line-height: normal 是基於字型度量計算出來的

  • line-height: n (n=1,2,3…) 可能得出一個比 virtual-area 還要矮的content-area

  • vertical-align 不靠譜

  • line-box 的高度的受其子項目的 line-height 和 vertical-align 的影響

  • 我們無法輕易的用 CSS 來控制字型度量

相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.