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 を再計算させ、その値を再び高さに設定する。一見すると直感的だが、実は問題がいくつかある。
- 入力のたびに 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 制限を加える場合や、コードから動的に内容を書き換える場合の対応はまだ含まれていない。
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 }}
/>
主な目的は、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';
// 最後の文字が改行なら、空白を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-height や max-height を付ければいい。僕は rows で高さを設定するのが好きだが、field-sizing: content を入れると rows と cols は効かなくなる。
rows and cols attributes modify the default preferred size of a
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> の例を挙げると。
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行で置き換えられると知ったときに近い。ブラウザがこうしたよくある要望を少しずつネイティブ実装に取り込んでいけば、開発者は本当に重要なことに集中できる。
関連記事
- 超リンクの下線をもっときれいに見せる:text-underline-offsetデフォルトでは下線が文字にかなり近く、こういう見た目を好まないデザイナーもいる。僕自身も、あまりきれいだとは思っていない。
- なぜウェブは Pixel Perfect を追求すべきではないのかPixel Perfect が本当に重要なときだけそれを気にすべきであり、そうでなければ往々にして双方にとって損な結果になる。
- CSS の HSL で色を書こう!(そしてより良い方法)Web開発において、従来の HEX や RGB の色表現は広く使われているものの、読みやすさや直感性に欠ける問題があり、P3 のような広色域では表現力にも限界がある。HSL(色相、彩度、明度)は、より直感的に色を定義できる方法であり、開発者が色を理解し調整しやすくしてくれる。HSL は色相・彩度・明度の3つの軸で色を表すため、特にデザインシステムにおいて、カラーパレットの明度変化をより自然に扱える。
- line-heightを1およびellipsisを設定しないようにしたい本文では、なぜウェブデザインにおいて line-height を 1 に設定することが推奨されないのか、また ellipsis を使用する際に直面する言語の問題について探ります。