# 読み上げられないフォームの送信結果

Source: https://easeweb.dev/learn/form-results
Topics: ライブ更新 > フォーム送信の結果が読み上げられる (WCAG 4.1.3)
The fix: 成功も失敗も、送信より前から空の状態でページにあるステータス領域に、結果を言葉で書き込みます。フォームの下の行に role="status" を付け、フォーカスは「登録する」ボタンに残します。
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://www.w3.org/WAI/tutorials/forms/notifications/ https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/status_role

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

## 場面

書店のサイトのフッターに、小さなメールマガジンの登録欄があります。「メールマガジンに登録」の見出し、「メールアドレス」の欄、「登録する」ボタンです。送信は裏側で処理され、ページは再読み込みされません。1秒ほどで、その下に「ありがとうございます。確認メールをお送りしました。」と表示されます。書店のメール配信サービスが止まっていれば、同じ行に「登録できませんでした。もう一度お試しください。」と出ます。画面が見える人は、その一文を読んで次へ進みます。スクリーンリーダーの利用者は、「登録する」を押しても、成功でも失敗でも何も聞こえません。フォーカスはボタンに残り、ボタンの名前も「登録する」のままで、下の行は読まれません。もう一度押して確認メールが2通届くか、登録できたか分からないままページを離れます。

## 確認できる問題

「ありがとうございます。確認メールをお送りしました。」はステータスメッセージです。利用者の操作の結果を伝え、フォーカスは移しません。達成基準4.1.3の例として、W3Cは「フォームを送信すると、送信できたことを伝える文がフォームに追加される」場面を挙げています。この基準が求めているのは、そうしたメッセージを役割やプロパティで支援技術に伝え、フォーカスを動かさなくても読み上げられるようにすることです。この例では、結果を `role="status"` も `aria-live` もないただの `p` に書き込んでいるため、ブラウザーは変化があったことをスクリーンリーダーに知らせません。これが失敗例F103です。失敗のメッセージもステータスメッセージで、同じように基準を満たしていません。

## 利用者への影響

メッセージはフォームのすぐ下にあるため、フォームを見ている人には見えます。見落とすのは、音声や点字で伝わる内容に頼っている人だけです。フォームがメール配信サービスの埋め込みなら、そのフォームを載せたどのページでも、同じように何も読み上げられません。

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

スキャンはページを読み込み、ラベルのある欄と名前のあるボタンを持つフォームを見て合格にします。フォームは送信しません。本番のサイトで送れば、実際に誰かが登録されてしまうからです。そのため結果が現れるところを見ることはなく、空の段落からは、そこに結果が入ることも分かりません。EaseWebの監査は、フォームがあり、ページのどこにもライブリージョンがない場合を、手で確認すべき手がかりとして報告します。クッキーバーなど、ほかの場所にライブリージョンがあるページではこの手がかりが出ないため、やはり人が送信して確かめる必要があります。修正を頼まれたAIアシスタントは、段落に文言を入れるのと同時に `aria-live` を付けることがあります。これでは領域と文言が一緒に現れます。スクリーンリーダーを動かしたまま、成功と失敗の両方で実際に送信して、初めて何が読まれるか分かります。

## 修正前：ただの段落に書き込まれる結果

```html
<section aria-labelledby="signup-title">
  <h2 id="signup-title">メールマガジンに登録</h2>
  <form id="signup">
    <label for="signup-email">メールアドレス</label>
    <input id="signup-email" name="email" type="email" autocomplete="email" required>
    <button>登録する</button>
  </form>
  <p id="signup-message"></p>
</section>
```

```js
const signup = document.getElementById('signup');
const message = document.getElementById('signup-message');

signup.addEventListener('submit', async (event) => {
  event.preventDefault();
  try {
    await subscribe(signup.email.value);
    message.textContent = 'ありがとうございます。確認メールをお送りしました。';
  } catch {
    message.textContent = '登録できませんでした。もう一度お試しください。';
  }
});
```

段落は最初からページにありますが、ライブリージョンではないため、書き込んでも変わるのは見た目だけで、読み上げは何も変わりません。フォーカスは変化のない「登録する」に残るので、そこにもスクリーンリーダーが新しく読むものはありません。

## 修正後：フォームの下のステータス行

```html
<section aria-labelledby="signup-title">
  <h2 id="signup-title">メールマガジンに登録</h2>
  <form id="signup">
    <label for="signup-email">メールアドレス</label>
    <input id="signup-email" name="email" type="email" autocomplete="email" required>
    <button>登録する</button>
  </form>
  <p id="signup-message" role="status"></p>
</section>
```

```js
const signup = document.getElementById('signup');
const message = document.getElementById('signup-message');

// 行を空にしてから少し待って書き込む。同じ文言を2回書き込んでも、変化として伝わるように。
function announce(text) {
  message.textContent = '';
  setTimeout(() => (message.textContent = text), 100);
}

signup.addEventListener('submit', async (event) => {
  event.preventDefault();
  try {
    await subscribe(signup.email.value);
    announce('ありがとうございます。確認メールをお送りしました。');
  } catch {
    announce('登録できませんでした。もう一度お試しください。');
  }
});
```

もとからある段落に `role="status"` を付けるので、文言が変わるときには、ブラウザーがすでにこの領域を監視しています。スクリーンリーダーは読み上げ中の内容を終えてから「ありがとうございます。確認メールをお送りしました。」と読みます。フォーカスは「登録する」に残り、利用者はそのまま続けられます。失敗も同じように読まれます。`announce()` は行を空にし、0.1秒後に文言を書き込むため、同じ文言の2回目の失敗も変化になります。この間がないと、オフラインのようにすぐ失敗するリクエストでは、空にするのと書き込むのが同時に起き、2回目が読まれないことがあります。メッセージはフォームの下に残り、ボタンを押したばかりの人がもう見ている場所にあります。

## 実装の判断

**領域を先に用意します。** コンポーネントのフレームワークでは、ステータス行を空の状態でフォームと一緒に描画します。結果と一緒に現れる `{#if}` の中には置きません。メッセージと一緒に現れる領域は、読み上げられないことがよくあります。この失敗は[画面に出ても読み上げられないステータスメッセージ](/ja/learn/status-messages)で詳しく扱っています。

**読み上げの強さ。** `role="status"` はスクリーンリーダーの読み上げが終わるのを待ちます。ここではそれで十分です。利用者は「登録する」を押したばかりで、答えを待っているからです。`role="alert"` は、すぐに対応が必要なメッセージのためにとっておきます。理由は[定型の通知でもVoiceOverの読み上げを遮るアラート](/ja/learn/urgent-alerts)で説明しています。

**送信中。** ボタンは押せるままにして、送信中に押されても無視します。フォーカスのあるボタンに `disabled` を付けると、ブラウザーがフォーカスをページに落とすことがあり、利用者は自分の位置を見失います。サービスの応答が遅くなりうる場合は、同じステータス行に「登録しています…」と書き込み、結果で置き換えます。[読み上げられない読み込み中の表示](/ja/learn/loading-states)が読み込み中のメッセージに使っているのと同じやり方です。応答が来たら取り消す短いタイマーから書き込めば、すぐに応答が来たときに2回伝えずに済みます。

**欄のエラー。** ブラウザーやサーバーが受け付けないアドレスは、送信の結果ではなく欄のエラーです。[直し方がわからないフォームエラー](/ja/learn/accessible-form-errors)のように、欄のそばに、欄と結び付けて出します。項目の多いフォームで複数のエラーが出る場合は、[どの欄が誤りか示さないエラー概要](/ja/learn/error-summary)のように、すべての欄を挙げる概要が必要です。そのときステータス行では、送信できなかったことだけを伝えます。

**お礼のページ。** 再読み込みしたり新しいページへ移ったりするフォームは、ステータスメッセージではありません。新しいページのタイトルと見出しが、何が起きたかを伝えます。シングルページアプリでお礼の画面へ切り替わる場合は、新しい画面として伝える必要があります。[読み上げられないSPAの画面遷移](/ja/learn/route-changes)で扱っています。

**埋め込みのフォーム。** メール配信サービスやマーケティングサービスが提供するフォームには、独自のスクリプトと成功メッセージが付いてきます。中には、フォーカスを移さずにフォームを差し替えるものもあります。自分のページに埋め込んだ状態で確認してください。スクリプトを変えられない場合は、お礼のページへ移動させる設定がサービスにないか確かめます。

**修正にならないもの。** フォームがページに残るのにメッセージへフォーカスを移せば、読み上げられはしますが、直したいかもしれないフォームから利用者を引き離します。ステータス行なら、利用者を動かさずに済みます。フォーカスを移すのが修正になるのは、フォームが消える場合だけです。お礼の文に差し替わるフォームのガイドで扱っています。メッセージを書き込むのと同時に段落へ `aria-live` を付けるのは、ステータスメッセージのガイドのトーストと同じ失敗です。ライブリージョンと文言が一緒に現れます。ボタンの `title` や `aria-label` を変えても、確実には読まれず、失敗も伝わりません。

## 修正の確認

1. 送信する前にDOMを確認し、ステータス行が存在し、空で、`role="status"` を持ち、`display: none` や `aria-hidden` で隠れていないことを確かめる。
2. スクリーンリーダーを有効にし、正しいアドレスで送信する。読み上げ中の内容のあとに結果が一度読まれ、フォーカスは「登録する」に残る。
3. 開発者ツールでオフラインにして2回送信する。毎回、失敗が言葉で読まれ、フォーカスは「登録する」に残る。
4. 回線を遅くし（Slow 4Gなどのスロットリングのプリセット）、「登録する」を押す。送信中もフォーカスはボタンに残り、結果は届く前ではなく、届いたときに読まれる。

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

## 制限

ライブリージョンの動きは、スクリーンリーダーやブラウザーによって異なります。このパターンは、その場で送信し、送信後もページに残る短いフォーム一つを扱っています。フォームをお礼の文に差し替えるデザインでは、フォーカスのあったボタンも消えるため、ステータス行だけでは足りず、お礼の文へフォーカスを移す必要があります。この場合は[読み上げられないお礼の文に差し替わるフォーム](/ja/learn/thank-you-focus)で扱っています。カートへの追加など、フォームではない操作については[バッジしか変わらない「カートに入れる」ボタン](/ja/learn/action-confirmations)で、その場で更新される検索結果については[件数が読み上げられない検索・絞り込み結果](/ja/learn/search-results)で扱っています。埋め込みのフォームも含め、実際のフォームを、成功と失敗の両方で、利用者が使うスクリーンリーダーで確認してください。

---

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