CSS field-sizing — 用一行 CSS 讓表單元素自動調整大小
# 前端CSS field-sizing: content 一行就能讓 textarea 根據內容自動調整高度,不再需要 JavaScript。但在這個屬性出現之前,這件事並不輕鬆。
用 JavaScript 監聽 input 事件
最常見的做法是監聽 input 事件,然後透過 scrollHeight 來動態調整高度:
const textarea = document.querySelector('textarea')
textarea.addEventListener('input', () => {
textarea.style.height = 'auto'
textarea.style.height = `${textarea.scrollHeight}px`
})
這段程式碼的邏輯是:先把高度重設為 auto,讓瀏覽器重新計算 scrollHeight,再把 scrollHeight 的值設回去。這個做法看起來很直觀,但其實有幾個問題:
- 每次輸入都會觸發 reflow。先設
auto再設回來,瀏覽器需要重新計算版面兩次。畫面容易出現明顯的跳動 - 需要處理初始狀態。頁面載入時如果 textarea 已經有內容(例如編輯模式),你還得手動觸發一次
- 跟框架整合時容易踩坑。在 React 裡你得用
ref加上useEffect,在 Vue 裡可能要用nextTick,每個框架的處理方式都不太一樣
如果是用 React,會長這樣:
function AutoResizeTextarea(props) {
const ref = useRef<HTMLTextAreaElement>(null)
useEffect(() => {
const el = ref.current
if (!el) return
const resize = () => {
el.style.height = 'auto'
el.style.height = `${el.scrollHeight}px`
}
el.addEventListener('input', resize)
resize() // 初始化
return () => el.removeEventListener('input', resize)
}, [])
return <textarea ref={ref} {...props} />
}
為了一個「根據內容調整高度」的功能,你需要建立一個元件、管理 ref、綁定事件、處理清除。這還沒算上如果要加 maxHeight 限制、或是要支援程式碼動態修改內容的情境。
建立一個 shadow textarea
另外一種做法是再建立一個 Shadow Textarea,也就是 DOM 當中會有兩個 <textarea>。並且將兩者的 style 調整成一樣,只是 Shadow 用的 textarea 樣式會用 visibility: hidden 的技巧藏起來,不顯示在畫面上。
<textarea // visible
rows={minRows}
style={style}
/>
<textarea // hidden shadow
aria-hidden
readOnly
tabIndex={-1}
style={{ visibility: 'hidden', position: 'absolute', overflow: 'hidden', height: 0 }}
/>
主要是為了防止 textarea 在設定 height: auto 跟 height: ${scrollHeight} 的期間出現 Flicker。流程如下:
- 把看得見的
textarea計算好的寬度(computedStyle.width)給看不見的 textarea - 把看得見的
textarea的 value 給看不見的 textarea - 讀取看不見的
textarea的scrollHeight - 把計算後的高度給看得見的
textarea
記得更久以前也有人用 div 當作 shadow,我已經找不到連結了。不過方向是一樣的,透過建立一個隱藏版元素避免畫面上的 textarea 出現 flicker。
以下是核心邏輯,參考了 MUI 的 TextareaAutosize:
function syncHeight(textarea, shadow, minRows) {
const computedStyle = window.getComputedStyle(textarea);
shadow.style.width = computedStyle.width;
shadow.value = textarea.value || 'x';
// textarea 如果最後一個字是換行,補一個空白避免高度計算不準
if (shadow.value.endsWith('\n')) {
shadow.value += ' ';
}
const contentHeight = shadow.scrollHeight;
// 單行高度,用來計算 minRows
shadow.value = 'x';
const singleRowHeight = shadow.scrollHeight;
const outerHeight = Math.max(minRows * singleRowHeight, contentHeight);
textarea.style.height = `${outerHeight}px`;
}
完整的實作還需要處理 ResizeObserver 的監聽。MUI 的原始碼裡有一段特別的處理:在調整高度時先暫停 ResizeObserver,等下一個 frame 再重新監聽,避免觸發 "ResizeObserver loop completed with undelivered notifications" 錯誤。光是這一個邊際案例,就能看出用 JavaScript 維護 auto-resize 有多瑣碎。
field-sizing: content
現在 CSS 原生提供了 field-sizing 屬性,只要一行就能做到:
textarea {
field-sizing: content;
}
就這樣。瀏覽器會根據表單元素的內容自動調整大小。如果要預設一個高度的話,記得加上 min-height 或 max-height。我喜歡用 rows 來設定高度,但加入 field-sizing: content 之後 rows 跟 cols 是不起作用的。
rows and cols attributes modify the default preferred size of a textarea. As a result, rows/cols have no effect on textarea elements with field-sizing: content set. MDN
單純使用 field-sizing: content 的話,元素會無限制地隨著內容擴張。實務上你幾乎一定會搭配 min-width、max-width、min-height、max-height 來控制範圍:
textarea {
field-sizing: content;
min-height: 3lh;
max-height: 10lh;
min-width: 200px;
max-width: 100%;
}
我建議使用 lh 搭配一起設定,這樣一來意圖會清晰很多。lh 代表元素本身的 line-height。3lh 就是三行的高度,比起用 px 或 em 更語義化,也更不容易因為字體大小改變而跑版。
行為邏輯是這樣的:
- 內容不足
min-height時,維持最小高度 - 內容超過
min-height但不超過max-height時,高度隨內容增長 - 內容超過
max-height時,高度固定在max-height,出現捲軸
寬度的邏輯也一樣。以 <input> 為例,文字量少時維持 min-width,隨著輸入增加逐漸變寬,超過 max-width 就不再擴張:
input {
field-sizing: content;
min-width: 100px;
max-width: 400px;
}
可以用在哪些元素上
field-sizing 不只適用於 <textarea>,也支援 <input> 和 <select>:
/* input 會根據輸入的文字長度自動調整寬度 */
input {
field-sizing: content;
}
/* select 會根據選項的文字長度調整寬度 */
select {
field-sizing: content;
}
field-sizing 的瀏覽器支援
截至 2026 年 4 月,Chrome 123+、Edge 123+、Opera 109+ 已支援 field-sizing。Safari 在 Technology Preview 版本中加入了支援,Firefox 仍在開發中。1
整體來看主流瀏覽器的支援度正在快速跟上,但如果你的產品需要支援較舊的瀏覽器,目前還是需要保留 JavaScript 的 fallback。我目前遇到會加入一個判斷,看瀏覽器是否支援:
const isFieldSizingSupported = CSS.supports('field-sizing', 'content');
雖然程式碼要根據條件式區分比較麻煩一些,但用 CSS 畢竟還是能省不少不必要的 JavaScript,只要能加我還是會加。
總結
field-sizing: content 解決了一個前端工程師處理了十幾年的老問題。以前不管用什麼框架,本質上都是在 JavaScript 層手動同步內容高度。現在瀏覽器原生就能處理,省下的不只是程式碼量,還有維護這些 workaround 的心力。
這種感覺就跟當年發現 scroll-behavior: smooth 可以一行取代整個 smooth scroll library 一樣。瀏覽器慢慢把這些常見需求收進原生支援,開發者就能專注在真正重要的事情上。
Footnotes
相關文章
- 讓你的超連結底線更好看:text-underline-offset預設的情況下底線跟文字很接近,有些設計師不喜歡這種樣式,我自己也覺得沒有很好看。
- 為什麼網頁不應該追求 Pixel Perfect只有在 Pixel Perfect 很重要的時候才應該關注他,否則往往會讓你得到一個雙輸的局面。
- 用 CSS HSL 來寫顏色吧!(以及更好的方法)在網頁開發中,傳統的 HEX 和 RGB 顏色表示法雖然廣泛使用,但存在不易閱讀和直觀性不足的問題,且在廣域的色彩空間如 P3 下表現能力受限。HSL(色相、飽和度、亮度)提供了一種更直覺的顏色定義方式,讓開發者能更輕鬆地理解與調整顏色。HSL 通過色相、飽和度和亮度三個維度來描述顏色,使得顏色的調整更加人性化,特別是在設計系統中,HSL 能更好地呈現色盤的亮度變化。
- 盡可能不設定 line-height 為 1 與 ellipsis 的原因本文探討了為何在網頁設計中不建議將 line-height 設為 1,以及使用 ellipsis 時遇到的語言問題