411
325

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

内容に応じてサイズが可変する <textarea> を素敵に実装する

Last updated at Posted at 2019-12-15

概要

内容に応じてサイズが可変する textarea を、できるだけ手間をかけず、スマートな実装を試みます。
しかも、ネイティブのフォームが持っている利点をそのまま活かして、堅牢でアクセシブルな設計を目指します。

標準 textarea の難点

HTML の textarea 要素は基本的に高さが固定されていて使い勝手が悪いです。3行分くらいしか領域がなくて、長い文章を打つのがとにかく苦痛なんていうこともザラです。

最近のブラウザ実装では、多少気を利かせてくれているのか、テキストエリアの領域をドラッグで拡大・縮小できます。

textarea をマウスドラッグでリサイズしているところの動画。

ただ私は思うのです。めんどくさいし、最初っから、入力するテキスト量に応じて自動的に伸び縮みしてくれればいいのに……と。スクロールバーなんて、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 で絶対配置をし、同時に widthheight100% を指定することで、親要素の大きさと同一にしています。

.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 の大きさも決まるという按配です。

textarea がテキスト内容に応じて拡大している様子。

原理解説用の 3D 表示の FlexTextarea

基本はこれだけです。続いて、細かい部分を解説していきます。

最小・最大の高さを指定する

何もテキストが入力されていないときの最小の高さと、たくさん入力されたときの最大の高さを設定できます。設定しないことも可能で、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'
})

textareadummy の細かな挙動を統一する

ここが最も難所です。

テキストエリアに設定される大きさは、完全に dummy の表示に依存しています。そのため、「何文字で改行するか」「改行ごとにどれくらい大きさが変わるか」「禁則処理ルール」などの点を同一にしていかなくてはいけません。条件を同一にするためのファクターは以下のとおりです。

  1. フォント
  2. 文字サイズ
  3. 行の高さ
  4. 文字間隔
  5. 罫線の幅
  6. padding の大きさ
  7. 改行できない文字が連続したときの取扱い

これらを解決するために、以下のコードが必要になります。

.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;
}

テキストエリアにはたいてい、borderpadding の設定が必要になるでしょう。それにあわせて dummy にも同じ大きさで borderpadding を指定しています。また、ボックスの大きさ計算の基準をそろえるために 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 コンポーネントを皆さんお使いください。一番上に書いたものが完成系のコードです。

411
325
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
411
325

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?