深入理解CSS:font metrics, line-height 以及 vertical-align

發表於2017-04-28

Line-heightvertical-align 是比較簡單的CSS屬性,以至於我們大多數人都覺得完全理解這兩個屬性是如何工作以及如何使用它們。實際並非如此。這兩個屬性非常複雜,也許可以說是最難理解的屬性了。CSS有一個鮮為人知的特性:內聯元素格式化。這兩者恰好在這個特性上起著重要作用。

例如line-height 可以是一個長度或者是一個沒有單位的數值注1,但它的預設值是normalnormal又代表什麼呢?我們把它當作是1或者是1.2,甚至連CSS spec都沒有講清楚這個值是什麼。我們知道,無單位的line-heightfont-size相關聯的,但問題是font-size:100px在不同字型時表現是不一樣的,那line-height是相同還是不同的? 它的值真的在1到1.2之間嗎? 還有 vertical-align,它對line-height有什麼影響?

因此我們需要深入研究一下這個不那麼簡單的CSS機制。

讓我們先研究下font-size

看看這段簡單的HTML程式碼,包含3個,每個都有不同的 font-family.

使用同樣的font-size和不同的font-family會產生不同高度的元素:

圖1.不同font-family, 相同font-size, 高度不同

即便我們意識到這點,為什麼font-size: 100px 不能產生相同高度的元素呢?我測試過一些字型集,並得到以下值 Helvetica: 115px, Gruppo: 97px, Catamaran: 164px。

圖2.font-size為100px的元素高度從97px到164px不等

第一眼看上去有點不可思議,但實際上這就是這樣的。原因在於字型本身。它的機制是這樣:

  • 一個字型定義了它的em-square(或UPM,即每個em的單位)。也就是一個容器,每個字元將被繪製在容器裡。這個正方形使用相對單位,通常設定為1000單位,但也可以是1024,2048或其他任何值。
  • 根據字型的相對單位,設定字型的其他度量值(升部,降部,大寫高度,x字高等等)。請注意,某些值可能會超出這個方形容器。
  • 瀏覽器為了適應所需的字型大小,會縮放相對單位。

以Catamaran字型為例,並在[FontForge](https://fontforge.github.io/en-US/)中開啟,看一看其中的各項指標:

  • em-square是1000個單位的。
  • 升部是1100,降部是540。在一些測試後,看上去瀏覽器在Mac OS上的HHead Ascent/ Descent值,Windows上的Win Ascent/Descent值(這些值可能是不同的!)。 還有,Capital Height(大寫高度)是680,X height(X字高)是485。

圖3: 使用FontForge看到的字型各個度量值

這意味著Catamaran在1000個單位的容器中就用了1100 + 540個單位,因此使用這個字型時,如果設定font-size: 100px,那麼實際高度就是164px。這個計算出的高度定義了一個元素content-area內容區域,我將在本文的其餘部分引用這個術語。您可以將content-area理解為background屬性應用的區域注2

We can also predict that capital letters are 68px high (680 units) and lower case letters (x-height) are 49px high (485 units). As a result, 1ex = 49px and 1em = 100px, not 164px (thankfully, em is based on font-size, not computed height) 我們還可以預測,大寫字母是68px(680單位),小寫字母(X字高)高49畫素(485單位)。 因此,1ex = 49px和1em = 100px,而不是164px(謝天謝地,em單位是基於font-size,而不是計算後的高度)。

圖4: Catamaran字型:UPM-每單位em數-和畫素值在使用font-size:100px時值相同。

再更深入之前,我再簡要介紹一下這一機制。 當一個

元素在螢幕上呈現時,根據它的寬度定義,可以產生許多行。 每行由一個或多個內聯元素(HTML標籤或文字內容的匿名內聯元素)組成,稱為line-boxline-box的高度由其子節點的高度決定。 因此,瀏覽器會計算計算line-box(從其子節點的最高點到子節點的最低點)的高度,由此便有了每個內聯元素的高度。 因此(在預設情況下),line-box總是能夠容納其所有子節點。

每個HTML元素實際上是一堆line-box的集合。如果你知道每個line-box的高度,你就知道一個元素的高度。

如果我們把之前的程式碼改成下面這樣:

 

就會生成3個line-box

  • 第一行和第三行包含一個匿名的內聯元素(文字內容)。
  • 第二行包含兩個匿名的內聯元素,以及3個.

圖5:這個

段落(黑色邊框)由包含內聯元素(實體邊框)和匿名內聯元素(虛線邊框)的線框(白色邊框)共同組成。

我們可以很清楚地看到,第二個line-box比其他的高,因為其子節點的content-area更高,準確的說,是因為子節點使用了Catamaran字型。

問題在於line-box的建立的過程是黑盒的,也不能使用CSS來控制。即便使用:: first-line也不能讓我們設定第一個行內元素的高度。

line-height延伸出的問題

現在為止,我介紹了兩個概念:content-arealine-box。如果你看的很仔細,你就會發現我雖然說過一個line-box的高度是根據它的子節點的高度計算出,但我並沒有說它的子節點content-area的高度。這兩者有很大不同。

可能這聽起來很奇怪,一個內聯元素有兩個不同的高度:content-area 的高度和virtual-area 高度(我發明了這個術語virtual-area,因為這個高度不可見,但你在現在的規範裡找不到這個詞)。

  • (如前所述)content-area 的高度由字型度量值來定義。
  • virtual-area 高度就是line-height,它就是用來計算line-box的高度**

圖6:內聯元素的兩種不同高度

也就是說,人們通常的看法,即line-height是基線之間的距離,這一觀點是錯誤的。在CSS中,這並不成立注3

圖7:CSS裡,line-height並不是基線直接的距離。

virtual-areacontent-area之間的計算高度差異稱為行距。 這個行距一半在content-area的頂部,另一半在底部。 因此content-area始終位於virtual-area的中間

根據行距的計算值不同,line-heightvirtual-area)可以等於,大於或小於content-area。 在比virtual-area更小的情況下,行距為負,所以line-box在視覺上小於其子節點。

還有其他幾種內聯元素。

  • 被替換的內聯元素,(深入理解CSS:font metrics, line-height 以及 vertical-align, , 等等.)
  • inline-block 和所有 all inline-* 的元素
  • 參與特定格式化內容的內聯元素(例如,在flexbox元素中,所有flex元素都是blocksified

對於這些特定的內聯元素,高度是根據heightmarginborder這些屬性計算出的。 如果heightauto,那麼就使用line-heightcontent-area嚴格等於line-height

圖8:內聯替換的元素,inline-block、inline-*和blocksified的元素的內容區域等於其高度或行高

不過,我們面臨的問題仍然是:line-heightnormal值是多少? 可以在字型度量值中找到問題的答案,也就是content-area的高度計算。

讓我們回到FontForge。 Catamaran的em單位是1000,但是我們看到各種不同升部和降部的值:

  • 生成的升部/降部: 升部是770,降部是230。用於繪製字元。(表OS/2
  • 度量的升部/降部: 升部1100,降部是540。用於繪製 content-area的高度
  • 度量線距。在 line-height: normal時使用,在升部和降部之間的距離(表“hhea”)。

在這個例子中,Catamaran字型定義了0單位線距,所以line-height:normal將等於content-area,它是1640單位,或1.64

相比而言,Arial字型定義了2048個單位的大小,1854的升部,434的降部和67的線距。 這意味著font-size:100px會生成一個112px(1117個單位)的content-area和一個115px(1150個單位或1.15個)的line-height:normal。所有這些度量值都是字型特有的,由字型設計者設定。

很明顯,設定line-height: 1並不好。我要提醒你,無單位值是和font-size相關的,而不是content-area相關,而正是處理比content-area小的virtual-area的情況才是許多問題的起源。

圖9:line-height: 1,產生的line-box比content-area更小。

但這還不僅僅是line-height:1的問題。我的電腦上安裝了1117種字型(是的,我安裝了所有的字型從Google Web字型,1059種字型,大約95%,計算出的line-height都大於1。他們的line-height從0.618到3.378不等。你沒看錯,是3.378!

line-box計算的小細節:

  • 對於內聯元素, paddingborder會增加背景區域,但不會增加content-area的高度(也不是line-box的高度)。 因此content-area並不總是在螢幕上看到的內容。margin-topmargin-bottom無效。
  • 對於替換型的內聯元素,inline-blockblocksified inline元素來說,paddingmarginborder增加的是height,也就是content-arealine-box的高度。

##vertical-align:一個統領全域性的屬性

之前我並沒有提到vertical-align屬性,儘管它是計算line-box高度的一個重要因素。 甚至可以說**vertical-align可能在內聯內容格式化上有著重要作用。

vertical-align的預設值是baseline。你還記得字型指標裡升部和降部嗎? 這些值確定基線在哪裡,也確定他們的比例。由於升部與降部的比例很少為50/50,因此可能會產生一些比如對兄弟節點的影響。

還是從程式碼看起:

這個

元素含有兩個互為兄弟節點。他們繼承了font-family, font-size 以及固定 line-height的屬性。他們的基線會相同,並且這兩個元素的line-box高度都和他們的line-height行高相同。

圖10: 相同的字型,基線相同,萬事大吉

但如果第二個元素的font-size變小了呢?

聽上去這可能很奇怪,預設基線對齊方式可能會導致一個更高(!)的line-box,如下圖所示。你需要了解的是,line-box的高度是從其子節點的最高點到其子節點的最低點計算出來的。

圖11:較小的子元素會使line-box高度增加

這可能是[儘量使用line-height無單位值的依據](http://allthingssmitty.com/2017/01/30/nope-nope-nope-line-height-is-unitless/),但有時我們也需要固定值來[建立一個完美的垂直對齊的用例](https://scotch.io/tutorials/aesthetic-sass-3-typography-and-vertical-rhythm#baseline-grids-and-vertical-rhythm) 其實,無論你怎麼選,都會遇到內聯元素對齊的麻煩

讓我們看看另一個例子。一個line-height:200px

標籤,包含一個元素,子元素繼承了line-height的值。

line-box有多高? 我們的期望值應該是200px,但事實並非如此。其中的問題是

有自己的字型,不同於font-family(預設為serif)。

之間的基線可能會有所不同,因此line-box的高度比我們預期的高。這是因為瀏覽器進行計算時,會以每行line-box的一個零寬度字元開始,這一規範稱為strut。

一個看不見的字元,帶來看得見的效果

我們再回過頭來看一下之前提到兄弟節點的問題。

圖12:每個子元素都是對齊的,因為其line-box都從一個看不見且沒有寬度的字元計算出的。

但是基線對齊就不管用了。但是用vertical-align: middle可以解決這個問題麼?規範裡提到, middle意思是垂直方向上父節點的基線加上一半的x子高的總高度的中部對齊。基線比例是不同的,X子高的比例一樣,所以middle對齊並不可靠。更糟糕的是,在大多數情況下,middle絕對不會在正中間。有太多因素會影響對齊,無法通過CSS設定這些因素(x字高,升部/降部的比例等等)。

附註:還有4個其他值,在某些情況下可能有用:

  • vertical-align:top/bottom對齊到line-box的頂部或底部
  • vertical-align:text-top /text-bottom對齊到content-area的頂部或底部

圖13:Vertical-align對應四種值的情況

注意,在所有的情況下,都會對齊virtual-area,也就是那個不可見的高度。看一下這個簡單的例子,使用vertical-align:top不可見的line-height可能會產生奇怪但並意料之中的結果

圖14:垂直對齊可能會產生奇怪的結果,但是當你把行高視覺化後,結果其實是意料之中的

最後,vertical-align也可以是提高或降低與基線相關的數值的值。最後這個值可能會派上用場。

CSS 棒極了

我們已經討論了line-heightvertical-align的工作機制,那麼問題來了:字型度量值是否可以用CSS控制?簡單來說:不能,哪怕我十分希望可以。

不過無論如何,我們可以嘗試下。字型度量值是常量,所以我們應該能夠利用一下。

舉個例子,假如說我們要使用Catamaran字型的文字,讓文字高度高達正好是100畫素。似乎是可行的,計算一下好了。

首先,我們用CSS自定義屬性設定字型指標注4,然後通過計算font-size以獲得100px的高度。

圖15:大寫字元的高度正好是100px

很簡單,不是嗎? 但是,如果我們希望做到視覺上文字居中的效果,那麼剩餘的空間應該是需要平均分配在“B”字母的頂部和底部?為了實現這一點,我們必須基於升部/降部的比例來計算 vertical-align

首先,計算line-height:normalcontent-area的高度:

然後,我們需要:

  • 大寫字母底部到底部邊緣的距離
  • 大寫字母頂部到頂部邊緣的距離

就像這樣:

現在我們就可以計算vertical-align的值了,也就是距離乘以計算後font-size之間和底部的差值。 (我們必須將此值應用到內聯子元素上)。

最後,我們要設定所需的line-height,並計算如何保持垂直對齊:

圖16: 具有不同行高的例子。但文字都在中間。

現在再把一個和字元“B”等高的圖示加進去就很容易了

圖17: 圖示和字元等高

看看JSLint的結果

請注意,此測試僅用於演示目的。你不能依賴這一方法。原因有很多:

  • 除非字型指標是不變的,瀏覽器中的計算不是 ¯⁠_⁠(ツ)⁠_/⁠¯
  • 如果字型未載入,則預設字型可能具有不同的字型度量,並且處理多個值的計算邏輯將很快變得難以管理。

供你帶走的部分#

現在我們學習到了:

  • 內聯元素的格式化真的很難理解
  • 所有的內聯元素都有兩種高度

content-area(基於度量值) virtual-arealine-height行高) ** 毫無疑問,這兩個高度都不能視覺化。(如果你是一個devtools開發人員並且能夠解決視覺化問題,那就太棒了)

  • line-height: normal 是基於字型度量值的。
  • line-height: n 可能會創造一個比content-area更小的virtual-area
  • vertical-align不可靠。
  • 一個line-box的高度是基於它的子節點line-heightvertical-align屬性來計算的
  • 沒有什麼好辦法能用CSS簡單設定字型指標

但我依然愛CSS:)

資源


  • [注1]不管你怎麼選,都不是重點
  • [注2]這並不完全是這樣的。
  • [注3]在其他編輯軟體中,這可能是基線間的距離。 Word或Photoshop就是這樣。主要區別在於第一行也受CSS影響
  • [注4]您還可以使用前處理器中的變數,不需要自定義屬性

相關文章