# まったく読まれないか、1文字ごとに読まれてしまう文字数カウンター

Source: https://easeweb.dev/learn/character-count
Topics: ライブ更新 > 文字数カウンターが上限に近づいたことを伝える (WCAG 4.1.3)
The fix: 上限は欄に結び付けたヒントで伝え、見えている文字数はスクリーンリーダーから隠します。残りの文字数は、視覚的に隠したrole="status"の領域に、上限が近づいてから、入力が1秒ほど止まったときに書き込みます。キーごとに変わる文字数の表示に、aria-liveを付けないでください。
Test with: screen reader, keyboard
References: https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F103 https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA22 https://design-system.service.gov.uk/components/character-count/ https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-live

よくある問題を再現した学習用のサンプルです。実在のクライアントの診断結果ではありません。コードは出発点となる実装例のため、実際の製品でも確認してください。ここでは支援技術でのテスト結果は報告していません。

## 場面

雑貨を扱う小さなショップでは、注文の手続きでギフトメッセージを添えられます。欄に入るのは100文字までで、欄の右下には残りの文字数が「残り100文字」「残り99文字」と、入力に合わせて減っていきます。画面が見える人は、メッセージを入力してから下に目をやり、「残り15文字」を見て、あと一言書けると分かります。

スクリーンリーダーの利用者が同じメッセージを入力しても、聞こえるのは自分が入力した文字だけです。欄に上限があることも、あとどれだけ入るのかも分かりません。100文字に達すると、欄はそれ以上文字を受け付けません。書いていた文の終わりは入らず、その理由も何も伝えられません。残りの文字数を知るには、入力の手を止め、欄を離れて周りを読んで確かめるしかありません。

別の作りのショップでは、文字数の表示がassertiveのライブリージョンになっています。今度はキーを押すたびに「残り14文字」「残り13文字」「残り12文字」と数字が読まれます。スクリーンリーダーによっては、文字のあとに毎回読まれることも、読み始めたところで次のキーに遮られることもあります。どちらの場合も、文を入力するあいだ、ほぼ1文字ごとに数字が聞こえ、何を書いていたのかを見失ってしまいます。

## 確認できる問題

WCAGの定義では、ステータスメッセージとは、コンテキストの変化を伴わない内容の変化のうち、操作の結果、待ち状態、処理の進み具合、エラーのいずれかを伝えるものです。「残り15文字」は、押したキーの結果を伝え、フォーカスを受け取らずに変わります。達成基準4.1.3は、こうしたメッセージを役割やプロパティによって判別できるようにし、フォーカスを動かさずに支援技術が伝えられるようにすることを求めています。

1つ目の修正前の例では、文字数はただの `p` です。画面上の文言は変わっても、支援技術に変化を読むよう伝えるものがありません。W3CがF103として挙げている失敗例です。2つ目の修正前の例では、文字数の表示に `aria-live="assertive"` が付いています。これで変化はスクリーンリーダーに伝わりますが、すぐに読むべきメッセージという扱いです。しかもスクリプトは、キーのたびに新しい文字数を書き込みます。Understanding 4.1.3は、重要で急を要するわけでもない内容に `role="alert"` や `aria-live="assertive"` を使うことを失敗例に挙げ、ステータスメッセージは作業を不必要に妨げずに伝えるべきだとしています。残りの文字数は定型の知らせで、入力の手が止まるまで待てます。

## 利用者への影響

文字数の上限は、ギフトメッセージ、レビュー、プロフィールの自己紹介、問い合わせフォームによくあります。そして、ページの中で上限を示しているのが、この文字数の表示だけということも少なくありません。

## 自動チェックとAIの限界

スキャンはページを読み込み、マークアップの中に「残り100文字」を見つけます。欄に入力はしないため、表示が変わる様子は見えず、その行が入力した文字数を数えるものだということもHTMLからは分かりません。EaseWebの監査は実際に入力します。文字数の表示がある欄を上限の10文字手前まで埋めてから3文字入力し、ライブリージョンの変化を見ます。3回のキーのたびにassertiveの領域で文字数が書き換わると、問題として報告します。politeの領域でキーごとに書き換わる場合や、文字数の表示は変わったのに、入力中にもその後の待ち時間にも、どのライブリージョンにも何も書き込まれなかった場合は、人がスクリーンリーダーで確かめる項目として挙げます。文字数の表示をアクセシブルにするよう頼まれたAIアシスタントは、見えている文字数の表示に `aria-live` を `polite` か `assertive` で付けることがよくあります。読み上げられるようにはなりますが、キーを押すたびに読まれる、2つ目の修正前の例そのものです。スクリーンリーダーを動かしながら欄に入力して、何がいつ読まれるのかを聞いて、初めて分かります。

## 修正前：画面上でしか変わらない文字数

```html
<label for="gift">ギフトメッセージ</label>
<textarea id="gift" name="gift" maxlength="100"></textarea>
<p id="gift-count" class="count">残り100文字</p>
```

```js
function countText(left) {
  return `残り${left}文字`;
}

gift.addEventListener('input', () => {
  count.textContent = countText(100 - gift.value.length);
});
```

文字数は正しく、欄のすぐ下にあります。問題は一つです。

- `p` はライブリージョンではないため、文字数は画面上で変わるだけで読み上げられません。欄がいっぱいになっても、文字が入らなくなった理由は伝わりません。

## 修正前：assertiveのライブリージョンにある文字数

```html
<label for="gift">ギフトメッセージ</label>
<textarea id="gift" name="gift" maxlength="100"></textarea>
<p id="gift-count" class="count" aria-live="assertive">残り100文字</p>
```

```js
function countText(left) {
  return `残り${left}文字`;
}

gift.addEventListener('input', () => {
  count.textContent = countText(100 - gift.value.length);
});
```

文字数は読み上げられるようになりましたが、伝え方に問題があります。

- `aria-live="assertive"` は、新しい文字数をすぐに伝えるよう求めます。文字数は急ぎの知らせではありません。
- スクリプトがキーのたびに新しい文字数を書き込むため、ほぼ1文字ごとにメッセージが届きます。

## 修正後：上限が近いとき、入力が止まってから一度だけ伝える

```html
<label for="gift">ギフトメッセージ</label>
<p id="gift-hint" class="hint">100文字まで入力できます。</p>
<textarea id="gift" name="gift" maxlength="100" aria-describedby="gift-hint"></textarea>
<p id="gift-count" class="count" aria-hidden="true">残り100文字</p>
<p id="gift-status" class="visually-hidden" role="status"></p>
```

```js
function countText(left) {
  return `残り${left}文字`;
}

// 残り20文字からは、入力が1秒止まったときに一度だけ伝える
let pause;
gift.addEventListener('input', () => {
  const left = 100 - gift.value.length;
  count.textContent = countText(left);
  clearTimeout(pause);
  pause = setTimeout(() => {
    status.textContent = left <= 20 ? countText(left) : '';
  }, 1000);
});
```

1. 上限を欄の上のヒントに書き、`aria-describedby` で欄に結び付けます。利用者が欄に移動すると、入力を始める前に「100文字まで入力できます。」が読まれます。
2. 見えている文字数の表示から `aria-live` を外し、`aria-hidden="true"` を付けます。画面が見える人には今までどおりキーごとに変わって見えますが、キーのたびに読まれることも、ページを読んだときに二重に読まれることもなくなります。
3. `role="status"` の空の `p` を一つ、視覚的に隠して、最初からページに置きます。ここに書き込んだ内容は、スクリーンリーダーが読み上げ中の内容を読み終えてから伝えられます。
4. 残り20文字からは、入力が1秒止まったときに、そこへ文字数を書き込みます。入力しているあいだ文字数は聞こえず、上限の近くで手を止めたときに「残り12文字」と一度だけ聞こえます。上限から離れると領域を空にするため、次に上限へ近づいたときの文字数も、変化として伝わります。

これで2つの修正前の問題はどちらも直り、4.1.3を満たします。文字数は、役割を通じて控えめに、手が止まるたびに一度だけスクリーンリーダーへ伝わります。まったく伝わらないことも、キーのたびに伝わることもありません。

## 実装の判断

**どれだけ待つか。** この仕組みを備えたデザインシステムは、1秒ほど待っています。GOV.UKの文字数カウントは1秒ごとに確認し、キーが0.5秒以上押されていなければ、スクリーンリーダー向けのメッセージを更新します。米国政府のU.S. Web Design Systemは、最後のキーから1.2秒待ちます。これより短いと、ゆっくり入力する人には語の合間に文字数が聞こえ、長いと、次へ進んだあとで文字数が届きます。

**いつから伝えるか。** 短いメッセージを書く人にとって、1文字目から読まれる文字数は雑音です。GOV.UKのコンポーネントにはしきい値の設定があり、上限の75%など、決めた割合に達してから文字数を表示し、読み上げます。この例では残り20文字から伝えます。欄に移動したときにヒントで上限が読まれるため、上限が近づくまでは文字数を知る必要がありません。

**キーごとに書き換わる、politeの文字数。** 実際には、2つ目の修正前の例より、同じ文字数の表示に `aria-live="polite"` を付けた形のほうをよく見かけます。欄にフォーカスがあるあいだ、そう描画するコンポーネントライブラリーもあります。変化はどれも伝わるため、4.1.3の文言は満たしますが、キーのたびにメッセージを送ることに変わりはなく、ほぼ1文字ごとに文字数を読むスクリーンリーダーもあります。直し方は同じで、隠した領域を一つ置き、入力が止まってから書き込みます。

**途中で切らずに、上限を超えさせる。** `maxlength` があると、ブラウザーは上限で文字を受け付けなくなり、貼り付けた文のはみ出した部分も黙って切り捨てます。GOV.UKのコンポーネントは `maxlength` を使わず、上限を超えても入力できるようにしています。答えを最後まで入力したり貼り付けたりしてから削れるようにするためで、何文字超えているかは文字数の表示で伝えます。その場合も、送信の時点でフォームを止め、上限を超えていることを説明する必要があります。入力中に出すエラーは、[入力の途中で読まれてしまうか、まったく読まれないエラーメッセージ](/ja/learn/inline-validation)で扱っています。

**上限と同じ単位で数える。** `maxlength` と `value.length` はUTF-16のコード単位で数えるため、絵文字は2文字と数えられることがあります。バイト数で制限するサーバーでは、UTF-8で日本語のほとんどの文字が1文字3バイトになります。また、全角を2文字、半角を1文字と数えるサイトもあります。上限を実際に守らせる側と同じ単位で数え、その単位をヒントに書いてください。

**音声入力。** 音声認識ソフトの中には、スクリプトが待っているイベントを出さずに欄の値を変えるものがあります。GOV.UKのコンポーネントは、欄にフォーカスがあるあいだ1秒ごとに値も確かめ、音声で入力した文でも文字数が変わるようにしています。

**修正にならないもの。** 欄の `aria-describedby` で変わり続ける文字数の表示を指すと、欄へ移動したときには読まれますが、入力中には読まれません。聞こえるのは入力を始める前の文字数です。`role="alert"` は、2つ目の修正前の例と同じ問題を抱えます。文字数の表示へフォーカスを移すと、文の途中でカーソルが欄から抜けてしまいます。

## 修正の確認

1. 入力する前にDOMを確認し、見えている文字数の表示に `aria-hidden="true"` があって `aria-live` がないこと、`role="status"` の空の要素があり、`display: none` で隠れていないこと、欄の `aria-describedby` が上限を伝える文言を指していることを確かめる。
2. スクリーンリーダーを有効にして欄へ移動する。ラベルに続いて「100文字まで入力できます。」が読まれる。
3. 上限から遠いところで、語の合間に手を止めながら文を入力する。文字数については何も読まれない。
4. 残りが20文字を切るまで、いつもの速さで入力して手を止める。文字の合間に文字数は読まれず、手を止めたあとに残りの文字数が一度だけ読まれ、上限に達すると「残り0文字」が一度だけ読まれる。

OS、ブラウザーと支援技術のバージョン、ビルド、実施日、各手順の実際の結果を記録してください。入力欄のコンポーネント、文字数の表示、上限を変更した際は、再度確認します。

## 制限

ライブリージョンの動きは、スクリーンリーダーやブラウザーによって異なります。入力を再開すると、まだ読んでいない控えめなメッセージを捨ててしまうものもあります。このパターンは、文字数に決まった上限がある一つの欄を扱っています。語数で制限する場合も、数える単位が変わるだけで同じです。下書きの保存のようなほかのステータスメッセージは、[画面に出ても読み上げられないステータスメッセージ](/ja/learn/status-messages)で扱っています。実際の上限、ヒント、エラーメッセージを含めた実際の欄を、利用者が使うスクリーンリーダーで確認してください。

## スクリーンリーダーでの動き（EaseWebが実機で確認）

`role="status"`: polite のライブリージョンです。変化は、スクリーンリーダーが読み終えるまで待ちます。

| スクリーンリーダー | ブラウザーとOS | 下矢印キー | 連続読み上げ |
| --- | --- | --- | --- |
| NVDA | Chrome, Windows 11 | 未確認 | 未確認 |
| JAWS | Chrome, Windows 11 | 未確認 | 未確認 |
| VoiceOver | Safari, iOS | 未確認 | 未確認 |
| TalkBack | Chrome, Android | 未確認 | 未確認 |

`aria-live="assertive"`: 領域が変わるたびに、すぐに伝えるよう求めます。

| スクリーンリーダー | ブラウザーとOS | 下矢印キー | 連続読み上げ | 入力 |
| --- | --- | --- | --- | --- |
| NVDA | Chrome, Windows 11 | 読んでいるまとまりが終わってから読む (2026-10) | 未確認 | 未確認 |
| JAWS | Chrome, Windows 11 | 読んでいるまとまりが終わってから読む (2026-10) | 未確認 | 未確認 |
| VoiceOver | Safari, iOS | 未確認 | 未確認 | 未確認 |
| TalkBack | Chrome, Android | 未確認 | 未確認 | 未確認 |

`aria-describedby`: ページ内の別のテキスト（ヒントやエラーなど）を、要素の説明にします。

| スクリーンリーダー | ブラウザーとOS | Tabで移動 | 下矢印キー |
| --- | --- | --- | --- |
| NVDA | Chrome, Windows 11 | 未確認 | 未確認 |
| JAWS | Chrome, Windows 11 | 未確認 | 未確認 |
| VoiceOver | Safari, iOS | 未確認 | 未確認 |
| TalkBack | Chrome, Android | 未確認 | 未確認 |

https://easeweb.dev/ja/learn/screen-reader-support

---

このガイドをコーディングエージェントで使う: MCP https://easeweb.dev/mcp（設定は https://easeweb.dev/ja/services/developers）
