CSS field-sizing — たった1行のCSSでフォーム要素を自動リサイズする
CSSの field-sizing: content を1行書くだけで、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に設定してから戻すため、ブラウザはレイアウトを2回再計算する必要がある。画面に目に見えるチラつきが発生しやすい - 初期状態の処理が必要。ページ読み込み時に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 の制限をかけたり、プログラムによる値の動的な変更に対応したりするケースまで考慮すると、さらに面倒になる。
シャドウtextareaを作成する
もう1つの方法は、シャドウ用のTextarea(Shadow Textarea)を作成することだ。つまりDOM内に2つの <textarea> を配置する。そして両者のスタイルを一致させつつ、シャドウ用の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} を設定する間にフリッカー(画面のチラつき)が発生するのを防ぐためだ。流れは以下のようになる:
- 表示されている
textareaの計算済み横幅(computedStyle.width)を非表示のtextareaに渡す - 表示されている
textareaのvalueを非表示のtextareaに渡す - 非表示の
textareaのscrollHeightを読み取る - 計算された高さを表示されている
textareaに適用する
もっと前には div をシャドウとして使う手法もあった記憶があるが、リンクはもう見失ってしまった。だが方向性は同じで、非表示の要素を作ることで画面上の textarea にフリッカーが発生するのを防ぐのだ。
以下がそのコアロジックで、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 を一時停止し、次のフレームで再監視することで、"ResizeObserver loop completed with undelivered notifications" エラーの発生を防いでいる。このエッジケースひとつをとっても、JavaScriptで自動リサイズを保守するのがどれほど煩わしいかがわかるだろう。
field-sizing: content
現在では、CSSにネイティブの field-sizing プロパティが提供されており、たった1行で実現できる:
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 は3行分の高さとなり、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によるフォールバックを残しておく必要がある。僕の場合は、ブラウザが対応しているかどうかを判定する処理を入れるようにしている:
const isFieldSizingSupported = CSS.supports('field-sizing', 'content');
条件分岐によってコードを分けるのは多少手間だが、CSSを使えるなら無駄なJavaScriptを大幅に削減できるため、使える場面なら僕は積極的に使っていきたい。
まとめ
field-sizing: content は、フロントエンドエンジニアが十数年にわたって向き合ってきた積年の課題を解決してくれた。以前はどんなフレームワークを使っていようと、本質的にはJavaScriptレイヤーでコンテンツの高さを手動で同期していた。今やブラウザがネイティブで処理してくれるようになり、削減できたのはコード量だけでなく、こうしたワークアラウンドをメンテナンスする労力も含まれている。
この感覚は、かつて scroll-behavior: smooth によってスムーススクロールライブラリ全体がたった1行で置き換えられると知ったときの感覚にとても似ている。ブラウザがこうしたよくある需要を徐々にネイティブサポートへと取り込んでくれることで、開発者は本当に重要な課題に集中できるようになるのだ。
Footnotes
関連記事
- Three.js で僕の部屋を表現する 僕が React Three Fiber を使って実際の自分の部屋をブラウザ上に再現し、実世界のオブジェクトを目録に見立て、空間の記憶を通してここ数年の生活と仕事について語った話。
- フロントエンドで画像を扱う際に注意すべきこと Jake Archibaldの記事を起点に、現代のレスポンシブ画像の書き方を整理する。なぜwidth/heightを付ける必要があるのか、CSSのaspect-ratioはいつ使うべきか、AVIFとWebPの選び方、そしてpicture/source/srcsetを使ったモバイル向け画像の切り替えについて。
- リンクの下線をもっと見栄え良くする:text-underline-offset デフォルトでは下線と文字が近すぎて、このスタイルを好まないデザイナーもいるし、僕自身もあまり綺麗ではないと感じていた。
- なぜウェブで Pixel Perfect を追求すべきではないのか Pixel Perfect はそれが本当に重要な場合にのみ意識すべきであり、そうでなければ往々にしてお互い損をする結果に陥ってしまう。