話說在前頭:文中的行為我在 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()讀到的還是原本的字級。不過元素的高度會跟著放大後的字變高,不會跟下面的內容重疊(字距是固定值時例外,見第五節)。
二、動手試試
下面有三個情境:活動主視覺的標語、中文和德文的「加入購物車」按鈕,以及會換行的標題。可以拉動容器寬度、切換寫法,也可以勾選「把預設字級調大一倍」,看文字實際被縮放到多大:
互動範例:調整容器寬度(虛線)、切換寫法,看文字實際被放大或縮小到多大
週年慶全館五折
偵測中(需要 JavaScript)
此處為互動範例,請在網頁上操作。
勾選「把預設字級調大一倍」之後,只寫 grow 的標語,實際字級幾乎不變;改成 grow per-line-all 200%,字才會跟著變大。「會換行的標題」可以按「找出會撐破容器的寬度」,看第三節的 bug。
三、無障礙實測:三個要小心的地方
1. 瀏覽器放大時,字反而變小
我用滿版的活動標語「週年慶全館五折」測試:容器跟視窗一樣寬,字級是 2rem。下表是字實際在螢幕上的高度:
| 寫法 | 放大 100% | 放大 200% | 放大 400% |
|---|---|---|---|
沒用 text-fit | 36px | 72px | 144px |
text-fit: grow | 201px | 195px | 185px |
text-fit: grow per-line-all 200% | 72px | 144px | 185px |
只寫 grow 時,放大頁面反而讓字越來越小。因為放大後,以 CSS 像素計算的視窗變窄了,grow 又會讓文字剛好撐滿這個變窄的寬度,結果就把放大的效果抵銷掉了。
這違反 WCAG 1.4.4(文字可放大到 200%)。CSSWG 也有一個還沒解決的 issue 在討論這件事:容器寬度跟著視窗走的時候,放大可能完全改變不了字的大小。
加上放大上限就好了。 grow 200% 讓文字最多放大到原本的兩倍,寬螢幕上不一定會撐滿,但放大頁面時,字會正常跟著變大。
2. 使用者調大的預設字級,被抵銷掉了
有些使用者會在瀏覽器設定裡把預設字級從 16px 調大。我把預設字級調成 32px 再測一次:
| 寫法 | 預設字級 16px | 預設字級 32px |
|---|---|---|
text-fit: grow | 201px | 201px(沒變) |
text-fit: grow per-line-all 200% | 72px | 144px |
只寫 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-line | 0 次 |
grow per-line-all | 0 次 |
會在哪個寬度發生跟字型有關,範例裡的寬度不一定跟我測的一樣,所以範例的按鈕會讓你的瀏覽器自己掃一遍。會換行的標題,目前建議改用 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 處理本來就不該擠在一起的內容 | 文字放不下時,通常應該先調整版面(換行、加寬、精簡文字),縮小字級是最後的手段 |
判斷原則
用之前可以先問自己兩個問題:
- 這段文字變大或變小,會不會影響使用者看懂內容? 會的話就不要用。
text-fit只適合「字級是裝飾性的」地方。 - 使用者放大頁面時,這段文字有沒有跟著變大? 一定要加百分比上下限,並實際放大到 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,一樣可能遇到,所以上面「太窄時改回換行」的退路很重要。 - 放大時,字可能蓋到上下的內容。 字只在畫面上放大了,排版高度卻沒有跟著變。我用 3 種字型、5 段文字、4 種寬度、2 種寫法測試,固定值的字距在 120 種組合裡有 30~41 種會發生,
六、不支援的時候
@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適合字少、字級屬於視覺設計的地方(大標語、大數字、多語系按鈕),不適合內文、一般標題層級和表單文字。
延伸閱讀
- width: 100% 為什麼會撐破容器?用 stretch 撐滿剛剛好
- 放大之後,置中的內容上半部不見了?用 safe center 讓它捲得到
- 現代 CSS 與 HTML 技巧整理清單:115 個特性、支援度與實驗優先序