· 5 分鐘閱讀

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 的值設回去。這個做法看起來很直觀,但其實有幾個問題:

  1. 每次輸入都會觸發 reflow。先設 auto 再設回來,瀏覽器需要重新計算版面兩次。畫面容易出現明顯的跳動
  2. 需要處理初始狀態。頁面載入時如果 textarea 已經有內容(例如編輯模式),你還得手動觸發一次
  3. 跟框架整合時容易踩坑。在 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: autoheight: ${scrollHeight} 的期間出現 Flicker。流程如下:

  • 把看得見的 textarea 計算好的寬度(computedStyle.width)給看不見的 textarea
  • 把看得見的 textarea 的 value 給看不見的 textarea
  • 讀取看不見的 textareascrollHeight
  • 把計算後的高度給看得見的 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-heightmax-height。我喜歡用 rows 來設定高度,但加入 field-sizing: content 之後 rowscols 是不起作用的。

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-widthmax-widthmin-heightmax-height 來控制範圍:

textarea {
  field-sizing: content;
  min-height: 3lh;
  max-height: 10lh;
  min-width: 200px;
  max-width: 100%;
}

我建議使用 lh 搭配一起設定,這樣一來意圖會清晰很多。lh 代表元素本身的 line-height。3lh 就是三行的高度,比起用 pxem 更語義化,也更不容易因為字體大小改變而跑版。

行為邏輯是這樣的:

  • 內容不足 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

  1. Can I Use

相關文章

探索其他主題