· 6分で読了

CSS field-sizing — 1行のCSSでフォーム要素を自動リサイズさせる

# フロントエンド
この記事は中国語から自動翻訳されたものです。翻訳によりニュアンスが失われている場合があります。

CSS field-sizing: content を使えば、textarea の高さを内容に応じて自動調整できる。JavaScript は不要で、たった1行で済む。ただし、このプロパティが登場する前は、同じことをやるのにかなりの手間がかかっていた。

JavaScript で input イベントを監視する

最もよくあるやり方は、input イベントを監視して scrollHeight を使い、動的に高さを調整する方法だ。

const textarea = document.querySelector('textarea')

textarea.addEventListener('input', () => {
  textarea.style.height = 'auto'
  textarea.style.height = `${textarea.scrollHeight}px`
})

このコードのロジックはこうだ。まず高さを auto に戻して、ブラウザに scrollHeight を再計算させ、その値を再び高さに設定する。一見すると直感的だが、実は問題がいくつかある。

  1. 入力のたびに reflow が発生するauto にしてから元に戻すので、ブラウザはレイアウトを2回再計算する。状況によっては画面のちらつきがかなり目立つ
  2. 初期状態の処理が必要になる。ページ読み込み時に textarea にすでに内容がある場合(編集画面など)、手動で一度発火させる必要がある
  3. フレームワークと組み合わせるとハマりやすい。React では refuseEffect を使う必要があり、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 をもう1つ作るやり方がある。つまり DOM 上に <textarea> が2つある状態にするわけだ。そして両者の 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 }}
/>

主な目的は、textareaheight: 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';

  // 最後の文字が改行なら、空白を1つ足して高さ計算のズレを防ぐ
  if (shadow.value.endsWith('\n')) {
    shadow.value += ' ';
  }

  const contentHeight = shadow.scrollHeight;

  // 1行分の高さ。minRows の計算に使う
  shadow.value = 'x';
  const singleRowHeight = shadow.scrollHeight;

  const outerHeight = Math.max(minRows * singleRowHeight, contentHeight);
  textarea.style.height = `${outerHeight}px`;
}

完全な実装にはさらに ResizeObserver のリスナー管理が必要になる。MUI のソースには注目すべき対処がある:高さを調整する間は ResizeObserver を一時停止し、次のフレームで再監視することで "ResizeObserver loop completed with undelivered notifications" エラーを回避している。このエッジケース1つだけでも、JavaScript で auto-resize を維持することがどれだけ面倒かがわかる。

field-sizing: content

今では CSS 標準で field-sizing プロパティが用意されていて、たった1行で実現できる。

textarea {
  field-sizing: content;
}

これだけだ。ブラウザがフォーム要素の内容に応じて自動でサイズを調整してくれる。デフォルトの高さを決めたいなら、min-heightmax-height を付ければいい。僕は rows で高さを設定するのが好きだが、field-sizing: content を入れると rowscols は効かなくなる。

rows and cols attributes modify the default preferred size of a

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 は3行分の高さであり、pxem よりも意味がはっきりしていて、フォントサイズの変更でレイアウトが崩れにくい。

挙動のロジックはこうだ。

  • 内容が min-height に満たない場合は、最小高さを維持する
  • 内容が min-height を超えるが max-height を超えない場合は、高さが内容に応じて伸びる
  • 内容が max-height を超える場合は、高さが max-height で固定され、スクロールバーが出る

幅のロジックも同じだ。<input> の例を挙げると。

input {
  field-sizing: content;
  min-width: 100px;
  max-width: 400px;
}

文字が少ないときは 100px を維持し、入力が増えるにつれて徐々に横に広がり、400px を超えるとそれ以上は広がらない。

どんな要素に使えるか

field-sizing<textarea> だけでなく、<input><select> にも対応している。

/* input は入力文字列の長さに応じて自動で幅が変わる */
input {
  field-sizing: content;
}

/* select は選択肢の文字列の長さに応じて幅が変わる */
select {
  field-sizing: content;
}

field-sizing のブラウザ対応

2026年4月時点で、field-sizing は Chrome 123+、Edge 123+、Opera 109+ で対応済みで、Safari も最新の Technology Preview 版でサポートが入っている。Firefox は現在も開発中だ。

全体として主要ブラウザの対応は急速に追いつきつつあるが、古いブラウザまでサポートする必要がある製品なら、今のところは JavaScript の fallback を残しておく必要がある。僕は今のところ、ブラウザが対応しているかどうかを判定する処理を入れている。

const isFieldSizingSupported = CSS.supports('field-sizing', 'content');

条件分岐に応じてコードを分けるのは少し面倒ではあるが、CSS で済むなら不要な JavaScript をかなり減らせる。追加できるなら、僕はやはり追加する。

まとめ

field-sizing: content は、フロントエンドエンジニアが十数年かけて向き合ってきた古い問題を解決してくれる。どんなフレームワークを使っていても、本質的には JavaScript 層で内容の高さを手動同期していた。今ではブラウザがネイティブで処理してくれる。節約できるのはコード量だけでなく、workaround を維持する労力そのものだ。

この感覚は、scroll-behavior: smooth で smooth scroll library を1行で置き換えられると知ったときに近い。ブラウザがこうしたよくある要望を少しずつネイティブ実装に取り込んでいけば、開発者は本当に重要なことに集中できる。

関連記事

他のトピックを探索