本文へスキップ

文字数カウンタ

テキストエリアの残り文字数をリアルタイム表示。上限に近づくと色が変わり、超過を防ぎやすい。

プレビュー

入力

操作実際に入力して試せます。

仕様書

向く場面
  • コメント・SNS 投稿など上限のある短文入力
  • 残り文字数を意識しながら調整させたい欄
  • 上限に近づくほど警告色を強めたい入力
避ける場面
  • 上限が無い自由記述欄
  • 上限が数千字で余裕が大きい入力(ノイズになる)
構成要素
  • ラベル
  • テキストエリア
  • 残り文字数(強調数字)
持っている状態
  • 通常
  • フォーカス
キーボード

Tab で欄へ移動し入力のたびに残り数を更新。maxlength により上限を超える入力はブラウザが止める

支援技術

label[for] で名前を結び、aria-describedby でカウンタを関連付ける。残り数は「残り N 文字」の文言で示し、色(30以下・10以下)は補助。段階を跨いだときだけ visually-hidden の aria-live=polite で通知。上限はネイティブ maxlength

動き

カウンタ色 200ms(警告段階の切替)。reduced-motion で無効

デザインの要点

  • 上限は maxlength にも書く。JS が止まってもブラウザが超過を防ぎ、カウンタは可視化に徹する
  • 警告は残り 30 と 10 の2段。1段だと「まだ余裕」と「もう危ない」の差が出ない
  • 読み上げは段階を跨いだときだけ。毎打鍵で「残り N 文字」を読ませると入力の邪魔になる
  • 数え方は value.length で maxlength と揃える。書記素で数えると、ブラウザが止める位置とずれる

使いどころ

入力量に上限があり、ユーザー自身に残り文字数を意識しながら書いてほしい場面に向く。コメント欄、SNS投稿、レビュー入力のように、上限に近づくにつれて調整が必要になる入力で効果が大きい。本実装はmaxlength属性も併用しているため、JSが何らかの理由で失敗してもブラウザ側の入力制限自体は生きる二重構えになっている点は評価できる。逆に、上限が無い自由記述欄や、上限があっても数千文字単位のように余裕が大きい入力には不要で、常時表示されるカウンタがかえって視線のノイズになる。また、日本語入力のように変換確定前の文字(IME入力中)は多くのブラウザでinputイベントのvalueに未確定文字を含む場合と含まない場合が分かれるため、厳密なリアルタイム性を求める用途では入力方式による誤差が起こりうる点を把握しておくべきである。

仕組み

inputイベントのたびにMAX(100)からarea.value.lengthを引いた残り文字数を計算し、cc-counterの中身をcreateElement/appendChildで組み立て直している。innerHTMLではなくDOM APIで組み立てているのは、ユーザー入力値を直接文字列結合しないことでXSSのリスクを避ける安全な書き方になっている。残り文字数に応じてcc-counterのclassNameを"cc-counter"→"cc-counter warn"(残り30以下)→"cc-counter danger"(残り10以下)の3段階で切り替え、CSS側の色だけで警告度を表現する。maxlength="100"がtextarea自身に設定されているため、ブラウザは既にそれ以上の入力を物理的にブロックしており、JS側のカウンタ表示はあくまで補助的な可視化であって、文字数を実際に制限しているのはネイティブのmaxlength属性である点が設計上のポイントになる。

実装の注意

残り文字数はJavaScriptの文字列lengthで数えているため、サロゲートペア(絵文字や一部の異体字)は1文字が2としてカウントされ、見た目の1文字に対して残り数が2つ減る不一致が起きる。Array.from(area.value).lengthに置き換えればサロゲートペアは1と数えられるが、これはコードポイント数であって、結合文字や家族絵文字・国旗のようにコードポイントが複数連なる1文字はまだ複数と数えてしまう。見た目の1文字(書記素)で数えたいなら Intl.Segmenter の granularity: "grapheme" で分割して数えるのが正確だ。次に、cc-counterのDOMをinput毎に毎回作り直す実装(createElement→appendChild)は、100文字程度の短い入力では問題にならないが、上限を大きくして頻繁なinputイベントで使う場合はtextContentの直接更新の方が軽い。また、maxlengthによるブラウザ側の制限とJS側の残り数表示は別々のロジックなので、将来どちらかだけ上限値(MAX定数とmaxlength属性)を変更すると数値が食い違う。2箇所の100という値は同期して変更する必要がある。

アクセシビリティ

textarea には aria-describedby="cc-counter" が付いており、フォーカスしたときにカウンタの内容(残り◯文字)が説明として読み上げられる。カウンタ自体は aria-live にしていない。入力の度に「残り 87 文字」を読み上げると煩雑になるため、代わりに視覚的に隠した #cc-live(aria-live="polite")を置き、warn(残り30)/danger(残り10)の閾値を跨いだ瞬間だけ「残り 30 文字」のように読ませている。こうすると打鍵中は静かで、注意が要る節目だけ知らせる形になる。色(warn/danger)による警告は色を認識しにくいユーザーには伝わりにくいが、"残り◯文字"というテキスト自体を常に出しているので、色以外の手がかりは確保できている。maxlengthはネイティブ属性なのでブラウザの入力ブロック自体はスクリーンリーダーにも一貫して効く。

調整して使う

調整しやすいのは3点。まず警告の閾値で、既定は残り30(warn)・10(danger)だが、上限が100より大きい場合は割合(例えば残り20%・5%)で計算する方が、上限を変えても警告のタイミングの体感が揃う。次に文言の向きで、現状は「残り◯文字」という減算型の表示だが、投稿系のUIでは「現在の文字数/上限」という表記(例: 42/100)の方が全体量を把握しやすいユーザーもいるため、用途に応じて切り替えられる。最後に、上限を超える直前ではなく超えた場合の挙動で、この実装はmaxlengthにより物理的に超過を防いでいるが、貼り付け(ペースト)で一時的に超過した値が入り、ブラウザによっては即座に切り詰められず一瞬超過状態を表示することがある。厳密に扱うならpasteイベントでも明示的にsliceする防御を足すとよい。

コード(コピーして使えます)

<div class="cc-wrap">
  <label class="cc-label" for="cc-area">コメント</label>
  <textarea
    id="cc-area"
    class="cc-area"
    maxlength="100"
    placeholder="100文字まで入力できます"
    rows="4"
    aria-describedby="cc-counter"
  ></textarea>
  <div class="cc-footer">
    <span class="cc-counter" id="cc-counter">残り <strong>100</strong> 文字</span>
  </div>
  <span class="cc-sr" id="cc-live" aria-live="polite"></span>
</div>

タグ

同じカテゴリ「フォーム入力」のほかのパーツ

関連パーツ