概要
内容に応じてサイズが可変する textarea
を、できるだけ手間をかけず、スマートな実装を試みます。
しかも、ネイティブのフォームが持っている利点をそのまま活かして、堅牢でアクセシブルな設計を目指します。
標準 textarea
の難点
HTML の textarea
要素は基本的に高さが固定されていて使い勝手が悪いです。3行分くらいしか領域がなくて、長い文章を打つのがとにかく苦痛なんていうこともザラです。
最近のブラウザ実装では、多少気を利かせてくれているのか、テキストエリアの領域をドラッグで拡大・縮小できます。
ただ私は思うのです。めんどくさいし、最初っから、入力するテキスト量に応じて自動的に伸び縮みしてくれればいいのに……と。スクロールバーなんて、1ページにひとつあればじゅうぶんなんですよ。
実装方法
難しいことはありませんが、HTML と CSS、JS が協調して動作します。
HTML
<label for="FlexTextarea">伸縮するテキストエリア</label>
<div class="FlexTextarea">
<div class="FlexTextarea__dummy" aria-hidden="true"></div>
<textarea id="FlexTextarea" class="FlexTextarea__textarea"></textarea>
</div>
CSS
.FlexTextarea {
position: relative;
font-size: 1rem;
line-height: 1.8;
}
.FlexTextarea__dummy {
overflow: hidden;
visibility: hidden;
box-sizing: border-box;
padding: 5px 15px;
min-height: 120px;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
border: 1px solid;
}
.FlexTextarea__textarea {
position: absolute;
top: 0;
left: 0;
display: block;
overflow: hidden;
box-sizing: border-box;
padding: 5px 15px;
width: 100%;
height: 100%;
background-color: transparent;
border: 1px solid #b6c3c6;
border-radius: 4px;
color: inherit;
font: inherit;
letter-spacing: inherit;
resize: none;
}
.FlexTextarea__textarea:focus {
box-shadow: 0 0 0 4px rgba(35, 167, 195, 0.3);
outline: 0;
}
JavaScript
function flexTextarea(el) {
const dummy = el.querySelector('.FlexTextarea__dummy')
el.querySelector('.FlexTextarea__textarea').addEventListener('input', e => {
dummy.textContent = e.target.value + '\u200b'
})
}
document.querySelectorAll('.FlexTextarea').forEach(flexTextarea)
コードの大半は CSS ですし、JavaScript もなんかしてる? ってくらい簡素なものです。ですがしっかり動きます。
動作サンプル
サイズ可変の textarea の動作サンプル(CodePen)
解説
コードこそ簡素なものですが、とりあげて解説したい箇所はたくさんあります。
動作原理の概要
ざっくりと説明します。textarea
要素は親要素の大きさに追随して大きさが決まるようになっています。position: absolute
で絶対配置をし、同時に width
と height
を 100%
を指定することで、親要素の大きさと同一にしています。
.Textarea__textarea {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
そして親要素がテキストの量に応じた大きさになるように、入力されたテキストを .Textarea__dummy
にそのまま流し込みます。
textarea.addEventListener('input', e => {
dummy.textContent = e.target.value
})
すると、高さを指定していない .Textarea__dummy
はテキスト量に応じて自動的に大きさが決まり、連動して textarea
の大きさも決まるという按配です。
基本はこれだけです。続いて、細かい部分を解説していきます。
最小・最大の高さを指定する
何もテキストが入力されていないときの最小の高さと、たくさん入力されたときの最大の高さを設定できます。設定しないことも可能で、min-height
を指定しなければ1行分の高さになり、max-height
を指定しなければ、内容に応じてどこまででも大きくなります。個人的には min-height
の指定のみで良いだろうと思います。
.FlexTextarea__dummy {
min-height: 120px;
max-height: 480px;
}
入力された改行を改行として表示する
テキストエリアの値をそのまま .Textarea__dummy
に流し込むと、改行が半角スペースに変化してしまいます。これは DOM において、改行文字はホワイトスペースとして等価に扱われてしまうためです。とても有名な話ですが、HTML で改行をするには <br>
タグを使う必要があります。
ですが、改行文字を改行として反映するための CSS プロパティがあります。
.FlexTextarea__dummy {
white-space: pre-wrap;
}
こうしておけば、次のような単純なテキスト代入でも、改行文字が改行として表示されるようになります。
textarea.addEventListener('input', e => {
dummy.textContent = e.target.value
})
「改行文字を <br>
に文字列置換して、innerHTML
に代入する」ようなアプローチは、セキュリティ脆弱性を孕みやすいので、ぜったいにやめてね!!
末尾の改行を無視されないようにする
white-space: pre-wrap
はとても便利なプロパティですが、落とし穴が1点あります。テキスト末尾の改行が表示に現れないのです。たとえばテキストエリアに「TEST<改行><改行><改行>」と入力したときに dummy
に期待する高さは4行ぶんの高さですが、3行分の高さにしかなりません。
そこで、ゼロ幅スペース(U+200B)を末尾に埋め込むことで、末尾の改行も高さに含まれるようにしています。ゼロ幅なので、テキストのフローに影響することはありません。
textarea.addEventListener('input', e => {
dummy.textContent = e.target.value + '\u200b'
})
textarea
と dummy
の細かな挙動を統一する
ここが最も難所です。
テキストエリアに設定される大きさは、完全に dummy
の表示に依存しています。そのため、「何文字で改行するか」「改行ごとにどれくらい大きさが変わるか」「禁則処理ルール」などの点を同一にしていかなくてはいけません。条件を同一にするためのファクターは以下のとおりです。
- フォント
- 文字サイズ
- 行の高さ
- 文字間隔
- 罫線の幅
- padding の大きさ
- 改行できない文字が連続したときの取扱い
これらを解決するために、以下のコードが必要になります。
.FlexTextarea__dummy {
box-sizing: border-box;
padding: 5px 15px;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
border: 1px solid;
}
.FlexTextarea__textarea {
box-sizing: border-box;
padding: 5px 15px;
border: 1px solid #b6c3c6;
font: inherit;
letter-spacing: inherit;
}
テキストエリアにはたいてい、border
や padding
の設定が必要になるでしょう。それにあわせて dummy
にも同じ大きさで border
と padding
を指定しています。また、ボックスの大きさ計算の基準をそろえるために box-sizing: border-box
も必要です。
文字周りをそろえるために、font: inherit
, letter-spacing: inherit
を指定します。これで、フォント、ウエイト、行の高さが揃います。letter-spacing: inherit
も必要です。
テキストエリアは「aaaaaaaaaaaa……」と入力されたとしても、矩形を突き出ず、必ず改行される仕様になっています。dummy
もそれにあわせるため、 overflow-wrap
と、後方互換のための word-wrap
プロパティも設定します。
手動リサイズは行わないようにする
テキストエリアが自動的にリサイズされるようになったので、右下に表示されているリサイズハンドルは不要になりました。
.FlexTextarea__textarea {
resize: none;
}
フォーカスリングを表示する
伸縮可能かどうかに関係はありませんが、テキストエリアがフォーカスを受け取ったときに、ちゃんとそれとわかるインジケーターを表示してあげましょう。たいていはデフォルトの outline
で良いでしょう。余裕があれば独自のインジケーターを表示させるとカッコいいでしょう。
.FlexTextarea__textarea:focus {
box-shadow: 0 0 0 4px rgba(35, 167, 195, 0.3);
outline: none; /* box-shadow を代わりに使っているから outline は不要 */
}
スクリーンリーダー等の支援技術に配慮する
.FlexTextarea__dummy
には、テキストエリアに入力された文字がそのまま流し込まれています。同要素には visibility: hidden
プロパティが設定されているとはいえ、念のため HTML にも、aria-hidden
属性を使って、この要素がコンテンツとは無関係であることを示しておきましょう。
<div class="FlexTextarea__dummy" aria-hidden="true"></div>
完成
このようにして出来上がった FlexTextarea コンポーネントを皆さんお使いください。一番上に書いたものが完成系のコードです。