# 入力の途中で読まれてしまうか、まったく読まれないエラーメッセージ

Source: https://easeweb.dev/learn/inline-validation
Topics: ライブ更新 > 入力中に表示されるエラーが適切なタイミングで読み上げられる (WCAG 4.1.3, 3.3.1)
The fix: 欄の確認は、値が変わって欄を離れたとき、または入力が1秒ほど止まったときに行い、キーを押すたびには行いません。どちらの場合も、最初からページにあるrole="status"の要素を欄の下に置いてエラーを書き込み、aria-describedbyで欄と結び付け、同時にaria-invalidを設定します。値が正しくなったら、すぐにエラーを消します。
Test with: screen reader, keyboard
References: https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html https://www.w3.org/WAI/WCAG22/Understanding/error-identification.html https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA21 https://www.w3.org/WAI/tutorials/forms/notifications/ https://www.nngroup.com/articles/errors-forms-design-guidelines/ https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-invalid

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

## 場面

紅茶の店のチェックアウトで、最初にメールアドレスを、その下に電話番号を尋ねています。アドレスはブラウザー側で確かめるため、誤りがあると、注文を送る前に欄の下へ「メールアドレスを「name@example.com」のような形式で入力してください」と表示されます。画面が見える人には、このメッセージが赤い文字で現れます。スクリーンリーダーの利用者に何が聞こえるかは、確認がいつ動くかによって変わります。この店のフォームは、よくある二通りの作り方の両方で作られています。

一つ目の作り方では、キーを押すたびに欄を確かめ、メッセージをアラートとして送ります。スクリーンリーダーの利用者が「s」と打つと、その文字に続けて、重なるように「メールアドレスを「name@example.com」のような形式で入力してください」と読まれます。まだ1文字しか打っていません。数文字先の「sam@s」はアドレスとして正しいためメッセージは消えますが、「sam@shop,」でまた誤りになり、そのあとに打つ文字に重なって、アラートがもう一度読まれます。今打っている途中のアドレスへのエラーなのか、打とうとしたアドレスへのエラーなのか、聞き分けられません。

二つ目の作り方では、欄を離れたときにだけ確かめ、メッセージを欄の下のただの段落に出します。同じ利用者がドットの代わりにカンマを打って「sam@shop,com」と入力し、Tabキーを押します。スクリーンリーダーは「電話番号（任意）、編集テキスト」と読むだけです。エラーは画面上に出ていますが、場所はもう離れた欄の下です。戻っても「メールアドレス、編集テキスト、必須、sam@shop,com」と読まれ、何かがおかしいことは伝わりません。注文が送れなくなって、初めて気づきます。

## 確認できる問題

フォーカスを動かさずに現れるエラーは、ステータスメッセージです。達成基準4.1.3は、操作の結果や「エラーの存在」を伝える変化をステータスメッセージと定義し、フォーカスを受け取らずに役割やプロパティによって利用者へ伝えられることを求めています。Understandingのページは、それを「作業を不必要に妨げない形で」行うべきだとし、重要でも急ぎでもない内容への `role="alert"` や `aria-live="assertive"` を失敗例に挙げています。一つ目の作り方では、キーを押すたびに確認が動き、メッセージが変わるたびにアラートへ書き込まれます。入力途中のアドレスへのエラーは、重要でも急ぎでもありません。利用者がこれから変えるはずの状態を伝えているだけです。ブラウザーはページの求めるとおりに動いています。アラートの領域は文言が変わればすぐに読まれ、利用者が単語を打っている途中かどうかは考慮されません。

二つ目の作り方では、メッセージを役割のない `p` に書き込むため、エラーが現れても何も読まれないステータスメッセージになり、今度は読まれないという理由で4.1.3を満たしません。達成基準3.3.1の目的も果たせません。3.3.1は、自動的に検出した入力エラーについて、エラーのある項目を特定し、エラーを文字で説明することを求めています。Understandingのページは、その目的を、エラーが起きたことに利用者が気づき、何が誤っているかを判断できるようにすることだとしています。この例では説明はあるものの、欄の下に表示された文字でしかありません。欄と結び付けるものがなく、`aria-invalid` も `aria-describedby` もないため、欄は誤りのないものとして読まれます。Understandingのページは、こうしたマークアップを3.3.1の要件とはしていません。とはいえ、それもライブリージョンもなければ、エラーを知る方法は欄の先を1行ずつ読み進めることだけです。Tabキーで問題なく進めたように思えたあとに、そうする人はいません。

## 利用者への影響

この問題に出会うのは、チェックアウトや会員登録のフォームです。スマートフォンで使われることが多く、欄の下のエラーが画面キーボードに隠れることもあります。

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

スキャンはフォームを読み込み、ラベルの付いた欄と、その下の空の段落を見つけます。何も入力しないため、エラーが現れる様子は見えず、それがいつ現れるかも分かりません。EaseWebの監査は実際に入力します。メール欄に4文字を打ってTabキーを押し、キーを押しているあいだにフォーム内のライブリージョンへ書き込みがあったときや、Tabキーのあと、どのライブリージョンにも入っていない新しい文字がフォームに現れたときに、人が確かめる項目として挙げます。その文字が欄のエラーかどうか、スクリーンリーダーで何が読まれるかは、人が確かめます。一つ目の作り方では、スキャンには `aria-invalid`、`aria-describedby`、`role="alert"` がそろった、正しく見えるマークアップが見つかることさえあります。React Hook Formのアクセシビリティの例が示すのもこの形で、フォームのエラーをアクセシブルにするよう頼まれたAIアシスタントが、この形を加えることもあります。問題はコードが動くタイミングにあり、スクリーンリーダーを有効にして欄に入力して、初めて分かります。

## 修正前：キーごとに確かめ、アラートで送るエラー

```html
<label for="email">メールアドレス</label>
<input type="email" id="email" name="email" required aria-describedby="email-error">
<p id="email-error" role="alert"></p>
…
```

```js
function errorFor(field) {
  if (field.validity.valueMissing) return 'メールアドレスを入力してください';
  if (field.validity.typeMismatch) return 'メールアドレスを「name@example.com」のような形式で入力してください';
  return '';
}

function check() {
  const message = errorFor(email);
  email.setAttribute('aria-invalid', message ? 'true' : 'false');
  emailError.textContent = message;
}

email.addEventListener('input', check);
```

メッセージの文言は正しく、欄とも結び付いています。問題は、読まれるタイミングです。

- キーを押すたびに確認が動くため、打ち終えていないアドレスの1文字目でエラーが出ます。
- 入力のあいだに正しくなったり誤りに戻ったりするたびに出たり消えたりし、出るたびにまた読まれます。
- アラートとして送られるため、打っている文字に重なって読まれることがあります。

## 修正前：欄を離れたときに確かめ、黙って出すエラー

```html
<label for="email">メールアドレス</label>
<input type="email" id="email" name="email" required>
<p id="email-error"></p>
…
```

```js
function errorFor(field) {
  if (field.validity.valueMissing) return 'メールアドレスを入力してください';
  if (field.validity.typeMismatch) return 'メールアドレスを「name@example.com」のような形式で入力してください';
  return '';
}

function check() {
  emailError.textContent = errorFor(email);
}

email.addEventListener('blur', check);
```

アドレスを打ち終えるまでエラーを待たせているのは、タイミングとしては正しいです。ただし、次の問題があります。

- 段落はライブリージョンではないため、エラーが出ても何も読まれません。
- 欄は無効とされておらず、メッセージとも結び付いていないため、欄に戻ってもエラーは読まれません。

## 修正後：欄を離れたときに確かめ、一度だけ伝える

```html
<label for="email">メールアドレス</label>
<input type="email" id="email" name="email" required aria-describedby="email-error">
<p id="email-error" role="status"></p>
…
```

```js
function errorFor(field) {
  if (field.validity.valueMissing) return 'メールアドレスを入力してください';
  if (field.validity.typeMismatch) return 'メールアドレスを「name@example.com」のような形式で入力してください';
  return '';
}

function check() {
  const message = errorFor(email);
  email.setAttribute('aria-invalid', message ? 'true' : 'false');
  if (emailError.textContent !== message) emailError.textContent = message;
}

// アドレスを打ち終えたとき、つまり値が変わって欄を離れたときに確かめる
email.addEventListener('change', check);

// エラーが出ているあいだは、正しくなった時点ですぐに消す
email.addEventListener('input', () => {
  if (emailError.textContent && !errorFor(email)) check();
});
```

1. 確認は `change` で動かします。`change` は値が変わったあとで欄を離れたときに発生するため、キーを押しているあいだは確かめません。誰も入力していない欄をTabで通り過ぎても、誤りとはされません。
2. メッセージの段落に `role="status"` を付け、最初から空のままページに置きます。控えめな領域なので、エラーはスクリーンリーダーが今の読み上げ（多くは次の欄の名前）を終えてから一度だけ読まれるはずですが、次の欄の名前に遮られることもあります（「制限」を参照）。
3. 欄の `aria-describedby` で段落を指し、同じ処理の中で `aria-invalid` を設定します。欄に戻ると、無効な入力としてエラーと一緒に読まれます。
4. メッセージは変わったときだけ書き込み、`input` ではアドレスが正しくなったときに消すだけにします。直せばエラーはすぐに消え、キーを押しているあいだに新しく読まれるものはありません。

これで修正前の二つの問題はどちらも直り、4.1.3と3.3.1を満たします。エラーは、利用者がその欄の入力を終えたあとに、フォーカスを動かさずに一度だけ伝わり、欄とも結び付いたままです。

## 修正後：入力しながら確かめ、手が止まったときに伝える

```html
<label for="email">メールアドレス</label>
<input type="email" id="email" name="email" required aria-describedby="email-error">
<p id="email-error" role="status"></p>
…
```

```js
function errorFor(field) {
  if (field.validity.valueMissing) return 'メールアドレスを入力してください';
  if (field.validity.typeMismatch) return 'メールアドレスを「name@example.com」のような形式で入力してください';
  return '';
}

function check() {
  const message = errorFor(email);
  email.setAttribute('aria-invalid', message ? 'true' : 'false');
  if (emailError.textContent !== message) emailError.textContent = message;
}

// 入力しながら確かめるが、手が止まってから
let pause;
email.addEventListener('input', () => {
  clearTimeout(pause);
  if (!errorFor(email)) check();
  else pause = setTimeout(check, 1000);
});
```

1. キーを押すたびに1秒のタイマーをやり直し、タイマーが終わったときに確かめます。自分のペースで入力している人には、手を止めるまで何も聞こえません。
2. アドレスが正しくなったら、その場で確かめます。直した時点でエラーが消えます。
3. メッセージの段落に `role="status"` を付け、最初から空のままページに置きます。エラーは、スクリーンリーダーの読み上げが終わったあとに一度だけ読まれます。
4. 欄の `aria-describedby` で段落を指し、同じ処理の中で `aria-invalid` を設定します。メッセージは変わったときだけ書き込むため、同じ誤りのまま手を止めても、もう一度は読まれません。

これで修正前の二つの問題はどちらも直り、4.1.3と3.3.1を満たします。アドレスの途中で1秒手を止めると、まだ不完全だと伝えられてしまいます。このような欄で早めに知らせるなら、避けられない代償です。

## どの方法を選ぶか

どちらも基準を満たし、利用者の手が止まったときにエラーを一度だけ伝えます。欄を離れたときに確かめる方法は、メールアドレスをはじめ、多くの欄に合います。入力している途中で「アドレスが不完全です」と聞いて助かる人はいません。エラーは、利用者が欄を離れて入力を終えたことを示した時点で伝わります。手が止まったときに確かめる方法は、新しいパスワードや、空いていなければならないユーザー名のように、入力しながら条件に近づけていく欄に合います。欄を離れるまで待たずに、手を止めて様子を確かめるたびに結果が聞こえます。同じメッセージの要素を使い、欄ごとに使い分けるフォームも多くあります。

## 実装の判断

**`blur` ではなく `change` を使います。** キーボードやスクリーンリーダーの利用者は、入力を始める前に、何を尋ねられているかを知るためにTabキーでフォームをひととおり移動することがよくあります。`blur` で確かめると、通り過ぎた欄ごとに確認が動き、まだ何も入力していない必須の欄がすべて誤りとされてしまいます。`change` は値が変わったときだけ発生します。空のまま残った必須の欄は、フォームを送るときに捉えます。そのときにも同じ確認を行い、まだ誤りのある最初の欄へフォーカスを移してエラーと一緒に読ませるか、エラーの概要を表示します。[どの欄が誤りか示さないエラー概要](/ja/learn/error-summary)を参照してください。

**メッセージは、それだけで意味が通じるようにします。** エラーは自分の欄ではなく、次の欄の名前のあとに読まれます。そのため「形式が正しくありません」ではなく「メールアドレスを…」のように、何についてのエラーかを含めます。文言については[直し方がわからないフォームエラー](/ja/learn/accessible-form-errors)で扱っています。

**メッセージの要素は、ページと一緒に描画します。** 文言と一緒に現れるステータス領域は、読まれないことがよくあります。コンポーネントのフレームワークでは、エラー用の要素を空のまま描画し、DOMから外さずに置いておきます。メッセージと一緒に要素を加える `{#if}` や `{errors.email && …}` の中には置きません。

**`role="alert"` が合う場面。** W3Cの達成方法ARIA19は、フォームを送ったときのエラーを `role="alert"` に書き込んでいます。利用者は入力を終えたと示しており、先へ進む前に対応が必要です。フォームを入力している途中に見つかったエラーはそうではないため、控えめな領域で伝えます。

**フォームのライブラリー。** 多くのライブラリーは確認のタイミングを決めており、その設定は見落としがちです。

- React Hook Formは、初期設定では送信時に確かめ、その後は値が変わるたびに確かめます。`onBlur` と `onTouched` のモードは欄を離れたときに確かめ、`onTouched` は最初に欄を離れたあと、値が変わるたびに確かめます。アクセシビリティの例ではメッセージを `role="alert"` に入れています。`role="status"` を使い、要素は描画したままにしてください。
- Formikは値が変わるたびと欄を離れたときに確かめ、チュートリアルでは、一度訪れた欄のエラーだけを表示しています。表示するメッセージの要素は、役割がなく、どこにも結び付いていないただの `div` です。
- どのライブラリーでも、付属のコンポーネントがアラートを加えるなら、メッセージの要素は自分で書き、ライブラリーのエラーの状態から欄に `aria-describedby` と `aria-invalid` を設定します。

**使われる前の欄を無効にしません。** 空の必須の欄だからといって、最初から `aria-invalid="true"` を付けて描画しないでください。誰も触れていない欄で「無効な入力」と読まれ、説明するメッセージもありません。`aria-invalid` は確認の結果から、確認が動いたあとにだけ設定します。

**修正にならないもの。** 誤りのある欄へフォーカスを戻すと、利用者は欄を離れられず、キーボードの利用者はそこから抜け出せなくなることがあります。`role="alert"` の代わりに `aria-live="assertive"` を使っても、同じ失敗です。メッセージを欄と結び付けても、どのライブリージョンにも入れず、利用者が戻ってくるのを当てにするのは、マークアップを整えただけの二つ目の修正前と同じです。戻るよう伝えるものがありません。

**文字数カウント**も、入力に合わせて変わるメッセージで、考え方は同じです。[まったく読まれないか、1文字ごとに読まれてしまう文字数カウンター](/ja/learn/character-count)を参照してください。

## 修正の確認

1. 入力する前に欄を確認し、エラー用の要素が `role="status"` を持ちalertの役割を持たないこと、空のままDOMにあること、欄の `aria-describedby` がそのidを含むこと、欄に `aria-invalid="true"` がないことを確かめる。
2. スクリーンリーダーを有効にし、ドットの代わりにカンマを打ったメールアドレスを、いつもの速さで入力する。キーを押しているあいだ、エラーについては何も読まれない。
3. Tabキーを押すと、次の欄が読まれ、そのあとにエラーが一度だけ読まれる。Shift + Tabで戻ると、欄が無効な入力としてエラーと一緒に読まれる。手が止まったときに確かめる欄では、アドレスの途中で1秒手を止める。エラーが一度だけ読まれ、変わるまではもう読まれない。
4. アドレスを直すと、正しくなった時点でエラーが消える。ページを再読み込みし、何も入力せずに空のフォーム全体をTabで移動する。誤りとして表示されたり読まれたりする欄はない。

OS、ブラウザーと支援技術のバージョン、ビルド、実施日、各手順の実際の結果を記録してください。フォームのライブラリーやその設定、欄のコンポーネントを変更した際は、再度確認します。

## 制限

フォーカスが移るときに書き込まれた控えめなメッセージは、スクリーンリーダーや次の欄の読み上げの長さによって、次の欄の名前のあとに読まれることもあれば、それに遮られることもあります。そのためエラーは欄とも結び付け、戻ったときに読まれるようにしています。このガイドは、ブラウザーの中で確かめる一つの欄を扱っています。メッセージの文言と、欄が誤りであることの示し方は[直し方がわからないフォームエラー](/ja/learn/accessible-form-errors)で、フォームを送ったときのエラーの一覧は[どの欄が誤りか示さないエラー概要](/ja/learn/error-summary)で、その場で送るフォームの結果は[読み上げられないフォームの送信結果](/ja/learn/form-results)で扱っています。実際のフォームを、そのライブラリーやサーバー側の確認も含めて、利用者が使うスクリーンリーダーで確認してください。

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

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

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

`role="alert"`: assertive のライブリージョンです。ARIAは、変化をすぐに伝えるよう求めています。

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

`aria-invalid="true"`: 値がチェックを通らなかった入力欄だと示します。

| スクリーンリーダー | ブラウザーとOS | Tabで移動 | 下矢印キー |
| --- | --- | --- | --- |
| NVDA | Chrome, Windows 11 | 未確認 | 未確認 |
| JAWS | Chrome, Windows 11 | 未確認 | 未確認 |
| 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）
