話說在前頭:文中的行為我在 Chromium 153 上實測過。text-fit 目前只有 Chromium 支援,規格也還在討論,細節請以 caniuse、MDN 為準。有講錯的地方歡迎指正。

前言

活動主視覺的大字標語、雜誌封面的標題,設計稿上常常是「字剛好撐滿整行」。設計師在 Figma 裡拉一下字級就對齊了,但網頁的寬度一直在變。以前要做到這件事,只能靠 JavaScript 一直量文字寬度、算字級(例如 FitText.js),視窗一縮放就要重算一次。

Chrome 150 加入了 text-fit,一行 CSS 就能讓文字自動放大或縮小到剛好填滿容器。我實測之後發現它很好用,但如果直接照最簡單的寫法用,使用者把頁面放大時,字反而會變小。

先看一下你的瀏覽器支援度:

你的瀏覽器支援度

  • text-fit偵測中(需要 JavaScript)

一、寫法

.hero-title {
  text-fit: grow; /* 放大到剛好撐滿容器寬度 */
}

.tab-label {
  white-space: nowrap; /* 不換行,才看得出縮小的效果 */
  text-fit: shrink;    /* 太長時縮小到剛好放得下 */
}

text-fit 最多可以寫三個值,順序固定:

位置值意思
第一個(必填)none/grow/shrink不縮放/放大到撐滿/縮小到放得下
第二個consistent(預設)/per-line/per-line-all多行時:所有行用同一個倍率/每行各自縮放,但最後一行不動/每一行都各自縮放
第三個百分比grow 時是放大上限,shrink 時是縮小下限
text-fit: grow per-line-all 200%; /* 每行各自放大,最多放大到原本的 2 倍 */
text-fit: shrink 80%;             /* 縮小,但不小於原本的 80% */

有兩點要注意:

  • 沒有 text-fit: auto。 我在技巧清單裡一開始寫錯了,瀏覽器不認得這個值,整條宣告會被忽略。
  • font-size 的計算值不會變。 縮放是在排版完成後才套用的,用 getComputedStyle() 讀到的還是原本的字級。不過元素的高度會跟著放大後的字變高,不會跟下面的內容重疊(字距是固定值時例外,見第五節)。

二、動手試試

下面有三個情境:活動主視覺的標語、中文和德文的「加入購物車」按鈕,以及會換行的標題。可以拉動容器寬度、切換寫法,也可以勾選「把預設字級調大一倍」,看文字實際被縮放到多大:

互動範例:調整容器寬度(虛線)、切換寫法,看文字實際被放大或縮小到多大

情境
text-fit

週年慶全館五折

偵測中(需要 JavaScript)

此處為互動範例,請在網頁上操作。

勾選「把預設字級調大一倍」之後,只寫 grow 的標語,實際字級幾乎不變;改成 grow per-line-all 200%,字才會跟著變大。「會換行的標題」可以按「找出會撐破容器的寬度」,看第三節的 bug。

三、無障礙實測:三個要小心的地方

1. 瀏覽器放大時,字反而變小

我用滿版的活動標語「週年慶全館五折」測試:容器跟視窗一樣寬,字級是 2rem。下表是字實際在螢幕上的高度:

寫法放大 100%放大 200%放大 400%
沒用 text-fit36px72px144px
text-fit: grow201px195px185px
text-fit: grow per-line-all 200%72px144px185px

只寫 grow 時,放大頁面反而讓字越來越小。因為放大後,以 CSS 像素計算的視窗變窄了,grow 又會讓文字剛好撐滿這個變窄的寬度,結果就把放大的效果抵銷掉了。

這違反 WCAG 1.4.4(文字可放大到 200%)。CSSWG 也有一個還沒解決的 issue 在討論這件事:容器寬度跟著視窗走的時候,放大可能完全改變不了字的大小。

加上放大上限就好了。 grow 200% 讓文字最多放大到原本的兩倍,寬螢幕上不一定會撐滿,但放大頁面時,字會正常跟著變大。

2. 使用者調大的預設字級,被抵銷掉了

有些使用者會在瀏覽器設定裡把預設字級從 16px 調大。我把預設字級調成 32px 再測一次:

寫法預設字級 16px預設字級 32px
text-fit: grow201px201px(沒變)
text-fit: grow per-line-all 200%72px144px

只寫 grow 時,不管基準字級多大,最後都會縮放到同一個寬度,所以使用者調大的設定完全沒有效果。

shrink 也一樣。按鈕寬度如果寫死成 300px,使用者把字調大之後,shrink 又會把字縮回去。容器寬度改用 rem 設定就不會有這個問題,因為容器也會跟著使用者的字級變大。

3. 換行時,第一行可能撐破容器

這是我測到的一個 bug。標題在窄螢幕換成兩行時,預設的 consistent 照規格應該取「最寬那一行」的倍率,讓每一行都不超出容器。但在某些寬度下,Chromium 153 會套用較短那一行的倍率:

  • 「週年慶全館商品五折起限時三天」,容器 320px 寬:第一行被放大到 800px 寬,頁面出現橫向捲動。

我用 4 段文字、200 到 700px 每 10px 測一次,一共 204 種情況:

寫法撐破容器
grow(預設 consistent)6 次
grow 150%6 次(有上限,但還是會溢出)
grow per-line0 次
grow per-line-all0 次

會在哪個寬度發生跟字型有關,範例裡的寬度不一定跟我測的一樣,所以範例的按鈕會讓你的瀏覽器自己掃一遍。會換行的標題,目前建議改用 per-line-all。

另外,WCAG 1.4.12 文字間距的部分,grow 沒問題:使用者加大字距時,grow 會把字縮小一點來抵銷,文字仍然完整顯示,沒有被切掉。shrink 則有一個例外,見第五節。

四、什麼時候適合用?

text-fit 的本質是讓字級跟著容器寬度走,而不是跟著內容的重要性或使用者的設定走。所以它適合「版面比字級重要」的少數地方,不適合拿來當全站的字級系統。

適合用

情境寫法原因
活動主視覺、Landing page 的大字標語grow + 上限字少、只有一行,設計上就是要撐滿整行
雜誌風格的文章封面標題grow per-line-all + 上限標題是視覺重點,換行時每行各自撐滿也是常見的編排
數據儀表板、價格方案卡片上的大數字grow + 上限數字要在卡片裡盡量大又不能超出,卡片寬度又會隨版面改變
多語系的按鈕、分頁標籤、導覽列shrink + 下限 + 換行退路翻譯後的字數無法預測,例如德文常比中文長好幾倍,稍微縮小就能放進同一行
使用者輸入的名稱(暱稱、商品名稱),放在固定寬度的卡片上shrink + 下限內容長度無法控制,偶爾出現的長字串不會撐破版面

不適合用

情境原因
內文段落每段的字級會因為行寬不同而不一樣,閱讀時字忽大忽小;內文也最需要尊重使用者調整的字級
一般的標題層級(h2、h3)標題大小是用來表達層級的。用 text-fit 之後,短的 h3 可能比長的 h2 還大,視覺上的層級就亂了
表單欄位的標籤、錯誤訊息、說明文字這些是使用者完成操作必須看清楚的文字,不能為了版面縮小
要維持一致字級的列表例如商品卡片的標題:每張卡片的字數不同,用 shrink 會讓每張卡片的字大小都不一樣,看起來很亂
用 shrink 處理本來就不該擠在一起的內容文字放不下時,通常應該先調整版面(換行、加寬、精簡文字),縮小字級是最後的手段

判斷原則

用之前可以先問自己兩個問題:

  1. 這段文字變大或變小,會不會影響使用者看懂內容? 會的話就不要用。text-fit 只適合「字級是裝飾性的」地方。
  2. 使用者放大頁面時,這段文字有沒有跟著變大? 一定要加百分比上下限,並實際放大到 200%、400% 檢查。

五、我建議的寫法

大標語(grow):

.hero-title {
  /* 字級用 rem:使用者調大預設字級時,基準會跟著變大 */
  font-size: 3rem;
  /* 字距用百分比:固定值的字距會讓放大後的字蓋到上下的內容 */
  letter-spacing: 2%;
  /*
    per-line-all:每行各自縮放,避開換行時第一行撐破容器的問題
    200%:最多放大 2 倍,瀏覽器放大時字才會跟著變大
  */
  text-fit: grow per-line-all 200%;
}

基準字級要設得夠大,200% 的上限才能涵蓋常見的螢幕寬度。

按鈕、分頁標籤(shrink):

.tab-list {
  container-type: inline-size; /* 讓下面的容器查詢可以判斷寬度 */
}

.tab-label {
  letter-spacing: 5%;   /* 字距用百分比,才會跟著字一起縮 */
  white-space: nowrap;  /* 先嘗試放在一行 */
  text-fit: shrink 80%; /* 最多縮小到 80%,避免字小到看不清楚 */
}

/* 容器太窄時,改回換行,不要硬縮小;門檻要用最長的翻譯實測決定 */
@container (width < 12rem) {
  .tab-label {
    white-space: normal;
  }
}

只加 shrink 80% 不夠:縮到 80% 還放不下時,文字會直接超出按鈕。要搭配容器查詢,太窄時改回換行。

另外兩個我實測踩到的坑:

  • display: flex 或 grid 的按鈕,text-fit 加在按鈕上沒有作用。 很多設計系統的按鈕會用 flex 讓圖示和文字對齊,這時要把文字包在 <span> 裡,把 text-fit 加在 span 上,並設定 min-width: 0。flex 子項目預設不會縮到比內容窄,不加的話 span 會被文字撐寬,就沒有空間可以縮。
.icon-button {
  display: inline-flex;
  align-items: center;
  gap: 0.25em;
}

.icon-button > .label {
  min-width: 0;         /* 讓 span 可以比文字窄,text-fit 才有空間縮 */
  white-space: nowrap;
  text-fit: shrink 80%;
}
  • 字距要改用百分比。 letter-spacing 用 px、em、rem 這類固定值時,我測到兩個問題:
    • 放大時,字可能蓋到上下的內容。 字只在畫面上放大了,排版高度卻沒有跟著變。我用 3 種字型、5 段文字、4 種寬度、2 種寫法測試,固定值的字距在 120 種組合裡有 30~41 種會發生,normal 和百分比一次都沒有。
    • 縮小時,shrink 可能整個放棄。 固定的字距不會跟著縮,字很多、按鈕又很窄時,光是字距加起來就比按鈕寬,shrink 會直接放棄,文字以原本的大小超出。

    改成百分比(以字級為基準,例如 letter-spacing: 5%)之後,兩個問題都沒有再出現。不過使用者依 WCAG 1.4.12 用工具加大字距時,通常是用固定值的 0.12em,一樣可能遇到,所以上面「太窄時改回換行」的退路很重要。

六、不支援的時候

@supports (text-fit: grow) {
  .hero-title {
    text-fit: grow per-line-all 200%;
  }
}

不支援的瀏覽器會顯示原本的字級,只是沒有撐滿整行。標題本來就要在這個字級下看起來正常,所以不需要再用 JavaScript 補。

結論

  • text-fit: grow 一行就能讓大標題撐滿整行,shrink 能讓太長的文字縮小到放得下,取代以前的 JavaScript 寫法。
  • 一定要加百分比上下限。 沒有上限的 grow 會抵銷瀏覽器放大和使用者的字級設定,放大頁面時字反而變小。
  • 會換行的標題用 per-line-all。預設的 consistent 在 Chromium 153 某些寬度下會讓第一行撐破容器。
  • shrink 要搭配下限和換行的退路,不要讓字小到看不清楚。
  • 用 text-fit 的元素,字距(letter-spacing)改用百分比。
  • text-fit 適合字少、字級屬於視覺設計的地方(大標語、大數字、多語系按鈕),不適合內文、一般標題層級和表單文字。

延伸閱讀

相關連結