# スクリーンリーダーで読めないプログレスバーとメーター

Source: https://easeweb.dev/learn/progress-bars
Topics: ライブ更新 > 進行状況バーやメーターが進み具合を伝える (WCAG 4.1.2, 4.1.3)
The fix: labelの付いたprogress要素を使うか、描いたバーにrole="progressbar"と名前を付け、塗りと一緒にaria-valuenowを設定するか、描いたバーを隠して値をそばにテキストで書きます。どの方法でも、区切りの進み具合と終了を、最初からページにあるrole="status"の領域で伝えます。
Test with: screen reader, keyboard
References: https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA25 https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/progress https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/progressbar_role https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/meter_role https://html.spec.whatwg.org/multipage/form-elements.html#the-meter-element https://htmhell.dev/adventcalendar/2022/5/

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

## 場面

問い合わせフォームでファイルを添付できます。report.pdfを選んで「アップロード」を押すと、ファイル名の下の灰色のバーが数秒かけて青く伸び、いっぱいになるとチェックと「アップロード完了」が表示されます。画面が見える人は、バーが伸びるのを見て待ちます。スクリーンリーダーの利用者には、ボタンを押しても何も聞こえません。探しに行っても、バーがまったく見つからないか、半分以上伸びているのに「プログレスバー、0%」と読まれます。始まったのか、あとどれくらいか、終わったのかが分からず、もう一度押したり、添付される前にフォームを送ったりしてしまいます。

## 確認できる問題

足りないものは2つあります。1つ目はバーそのものです。ひとつの形では、バーは2つの `div` 要素、つまり土台と、スクリプトが `width` を変える塗りでできています。ロールも名前も値もないため、支援技術からは何もないのと同じです。もうひとつの形では `role="progressbar"` が付いていますが、名前がなく、スクリプトは塗りを動かすだけで `aria-valuenow` を変えません。そのため、スクリーンリーダーは最後まで0%と読みます。達成基準4.1.2は、スクリプトで作られる部品の名前とロールを支援技術が判別でき、値の変化が支援技術に伝わることを求めています。値を表示するだけのバーが「ユーザーインターフェースコンポーネント」に当たるかは議論があり、axeは名前のないプログレスバーを1.1.1 非テキストコンテンツとして報告します。どちらの見方でも、バーの意味が支援技術に届く必要があり、修正方法は同じです。

2つ目は知らせです。「半分完了」や「アップロード完了」はステータスメッセージです。フォーカスを移さずに、処理の進み具合と結果を伝えるもので、4.1.3の対象です。その解説書でも、ときどき読み上げられるプログレスバーが例に挙がっています。プログレスバーはライブリージョンではないため、正しく作ったバーでも、値が変わったときに読み上げられるとは限りません。スクリーンリーダーが自分で追うかどうか次第です。たとえばNVDAは、初期設定でプログレスバーが変わるたびにビープ音を鳴らし、進むほど音が高くなります。一方、利用者がバーに移動するまで何も言わないスクリーンリーダーもあります。ビープ音では、アップロードが終わったことも、失敗したことも分かりません。

## 利用者への影響

バーは見ている人に向けて描かれるため、見ていない人には、どこまで進んだかも、終わった瞬間も伝わりません。アップロード中に次の入力欄へ進んだ、画面が見える人も同じです。

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

axeは名前のない `role="progressbar"` や `role="meter"` を報告しますが、`div` 要素で描いたバーにはチェックするロールがありません。また、HTML標準の `progress` 要素や `meter` 要素の名前は確認しません。ある瞬間のページしか見ないため、塗りが伸びても `aria-valuenow` が0のままであることや、終了が伝えられないことは、どのスキャンにも分かりません。バーを直すよう頼むと、コーディングアシスタントは `role="progressbar"` と3つの `aria-value*` 属性を固定の数字で付けることがあります。これでは無音が誤った値に変わるだけです。バーと割合の表示を `aria-live` で囲み、1%ごとに読ませる例もあります。アップロードの最中にスクリーンリーダーで聞いて、初めて分かります。

## 修正前：divで描いたバー

```html
<p>アップロード中：report.pdf</p>
<div class="bar"><div class="fill" style="width: 0%"></div></div>
<p class="done" hidden>アップロード完了</p>
```

```js
await upload(file, (percent) => {
  fill.style.width = `${percent}%`;
});
done.hidden = false;
```

バーにはロールも名前も値もないため、スクリーンリーダーにはファイル名のあとに何もありません。「アップロード完了」も、表示されるだけで読み上げられません。ここでの `upload()` はサイト独自のアップロード処理を表し、`XMLHttpRequest` の `upload` の進捗イベントのように、送信中に割合を知らせるものとします。

## 修正前：名前がなく、値が動かないprogressbarロール

```html
<p>アップロード中：report.pdf</p>
<div class="bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
  <div class="fill" style="width: 0%"></div>
</div>
<p class="done" hidden>アップロード完了</p>
```

スクリプトはdivで描いたバーと同じです。スクリーンリーダーはプログレスバーを見つけますが、名前がないため何のアップロードか言えません。塗りが伸びても `aria-valuenow` は0のままなので、最初から最後まで0%と読まれます。終了もやはり伝わりません。

## 修正後：HTML標準のprogress要素

```html
<label for="upload-bar">アップロード中：report.pdf</label>
<progress id="upload-bar" max="100" value="0">0%</progress>
<p class="done" hidden>アップロード完了</p>
<p id="upload-status" role="status" class="visually-hidden"></p>
```

```js
let said = 0;
try {
  await upload(file, (percent) => {
    bar.value = percent;
    bar.textContent = `${percent}%`;
    // 更新のたびではなく、4分の1ごとに伝える。
    const quarter = Math.floor(percent / 25) * 25;
    if (quarter > said && quarter < 100) {
      said = quarter;
      status.textContent = `${quarter}%アップロードしました`;
    }
  });
  done.hidden = false;
  status.textContent = 'report.pdfをアップロードしました。';
} catch {
  status.textContent = 'report.pdfをアップロードできませんでした。もう一度お試しください。';
}
```

ブラウザーにとって `progress` はプログレスバーです。labelが名前になり、`max` に対する `value` が支援技術に伝わるため、スクリーンリーダーで移動すると、名前、ロール、値が読み上げられます。要素の中のテキストは、バーを描けないブラウザー向けの代わりの表示で、名前にはなりません。ステータス領域は最初から空のままページにあるため、メッセージは控えめに、4分の1ずつ読み上げられ、最後に「report.pdfをアップロードしました」と読まれます。代わりに見た目の自由は減ります。バーはブラウザーが描くため、色を変えるには `::-webkit-progress-value` や `::-moz-progress-bar` のような、ブラウザーごとの疑似要素が必要です。

## 修正後：描画を残す、独自のprogressbarロール

```html
<p id="upload-label">アップロード中：report.pdf</p>
<div class="bar" role="progressbar" aria-labelledby="upload-label"
  aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
  <div class="fill" style="width: 0%"></div>
</div>
<p class="done" hidden>アップロード完了</p>
<p id="upload-status" role="status" class="visually-hidden"></p>
```

```js
let said = 0;
try {
  await upload(file, (percent) => {
    // 描画と値を一緒に変える。
    fill.style.width = `${percent}%`;
    bar.setAttribute('aria-valuenow', percent);
    // 更新のたびではなく、4分の1ごとに伝える。
    const quarter = Math.floor(percent / 25) * 25;
    if (quarter > said && quarter < 100) {
      said = quarter;
      status.textContent = `${quarter}%アップロードしました`;
    }
  });
  done.hidden = false;
  status.textContent = 'report.pdfをアップロードしました。';
} catch {
  status.textContent = 'report.pdfをアップロードできませんでした。もう一度お試しください。';
}
```

バーの描画はそのままで、ロールによってプログレスバーになります。`aria-labelledby` が画面にある行を名前にし、`aria-valuenow` は幅と同じコールバックで設定するため、2つがずれません。区切りと終了は同じステータス領域で伝えます。`progress` 要素が担っていたことはすべてスクリプトの役目になるため、コンポーネントを変更したら、値と描画が一致するか確かめてください。

## 修正後：値をテキストで書き、描画を隠す

```html
<p>アップロード中：report.pdf <span id="upload-percent">0%</span></p>
<div class="bar" aria-hidden="true"><div class="fill" style="width: 0%"></div></div>
<p class="done" hidden>アップロード完了</p>
<p id="upload-status" role="status" class="visually-hidden"></p>
```

```js
let said = 0;
try {
  await upload(file, (percent) => {
    fill.style.width = `${percent}%`;
    percentText.textContent = `${percent}%`;
    // 更新のたびではなく、4分の1ごとに伝える。
    const quarter = Math.floor(percent / 25) * 25;
    if (quarter > said && quarter < 100) {
      said = quarter;
      status.textContent = `${quarter}%アップロードしました`;
    }
  });
  done.hidden = false;
  status.textContent = 'report.pdfをアップロードしました。';
} catch {
  status.textContent = 'report.pdfをアップロードできませんでした。もう一度お試しください。';
}
```

バーは描画のまま支援技術から隠し、値は上の行にテキストで書きます。スクリーンリーダーはほかの行と同じように読み、画面が見える人にも数字が見えます。空の `div` 要素はもともと何も読まれませんが、`aria-hidden` を付けると、次の開発者にも意図が伝わります。既存のバーへの変更はこれが最小です。代わりにロールはなくなります。スクリーンリーダーはここにプログレスバーがあることを知らないため、自分で追うことはできません。たとえばNVDAは、ほかの2つの修正では鳴らすプログレスバーのビープ音を、この場合は鳴らしません。知らせはすべてステータス領域が担います。

## どの方法を選ぶか

3つとも、バーの進み具合を最新の言葉で伝え、同じステータス領域で区切りと終了を知らせます。HTML標準の `progress` 要素は、コードが最も少なく済みます。ロール、labelからの名前、値はブラウザーが用意し、スクリプトは `value` を設定するだけです。疑似要素で見た目を整えられるなら、これを選びます。独自のロールなら、デザインのとおりにバーを描けますが、塗りと一緒に属性を保つのはスクリプトの役目です。差し替えられないコンポーネントのバーや、土台と塗り以上の表現が必要なデザインに向いています。値をテキストで書く方法は、既存のバーへの変更が最も小さく、誰にでも数字が見えます。バーの周りの文言は変えられても、バーそのものは変えられないときに選びます。その場合、スクリーンリーダーにはプログレスバーとして伝わらず、NVDAが進み具合に応じて鳴らすビープ音もなくなることを受け入れることになります。

## 実装の判断

**1%ごとではなく、区切りで伝えます。** アップロードは何十回も進捗イベントを出し、そのたびにステータスメッセージを出すと、利用者は数字の列を聞き続けることになります。数秒で終わる処理なら、4分の1ごとが合います。大きなエクスポートのような長い処理では、10秒ほどごとか、「準備中」「アップロード中」「処理中」のような段階ごとに伝えます。2〜3秒で終わるなら、終了だけを伝えます。バーや、バーと一緒に変わる割合の表示に `aria-live` を付けないでください。変化がすべて読み上げられます。テクニックARIA25のようにバーのそばにライブのテキストを置く方法は、スクリプトがそのテキストを区切りでだけ更新するなら使えます。

**割合ではなく手順の場合。** チェックアウトや会員登録で「ステップ2/4」をバーで示す場合、値は割合ではありません。スクリーンリーダーは `aria-valuenow` を割合として読むことが多く、1〜4の範囲の2が「2パーセント」と読まれることがあります。`role="progressbar"` に `aria-valuetext="ステップ2/4：お届け先"` を設定するか、手順をリストで示し、今の手順に `aria-current="step"` を付けます。リストのほうがバーより多くを伝えられます。

**量が分からない場合。** どこまで進んだか分からないなら、値を省きます。`value` 属性のない `progress` 要素や、`aria-valuenow` のない `role="progressbar"` は、何かが進行中であることだけを示す不確定の状態です。これは読み込み中の表示であり、[読み上げられない読み込み中の表示](/ja/learn/loading-states)で別のパターンを扱っています。

**メーター。** メーターは、使用中のストレージやパスワードの強度のように、決まった範囲の中の測定値を示すもので、作業の進み具合を示すものではありません。HTMLの仕様も、プログレスバーに `meter` を使わないよう定めています。名前は `progress` と同じようにlabelで付けます。たとえば `<label for="storage">使用中のストレージ</label>` のあとに `<meter id="storage" min="0" max="10" value="7.2">` を置きます。メーターの読み上げはスクリーンリーダーによってばらつきがあります。2022年に公開されたテストでは、NVDAはメーターをプログレスバーと読み、macOSのVoiceOverはスタイルを付けたメーターを飛ばし、どれも低い値や高い値の区切りを読みませんでした。そのため、値をメーターのそばにテキストでも書き（「7.2 GB / 10 GB」）、色の意味も言葉で示します（「強度：弱い」）。独自のメーターには `role="meter"`、名前、`aria-valuenow`、`aria-valuemin` と `aria-valuemax`（既定値は0と100）、単位を含む `aria-valuetext` を付けます。

**色と強制配色。** 値を示すのが塗りだけの場合、塗りは意味を持つ図形なので、土台に対して3:1のコントラストが必要です。[コントラストが足りない入力欄・アイコン・フォーカス枠](/ja/learn/non-text-contrast)を参照してください。`background-color` で描いた塗りは、背景が置き換えられる強制配色のテーマでは消えてしまいます。`@media (forced-colors: active)` の中で `background-color: CanvasText` を指定します。

## 修正の確認

1. アップロード前にDOMを確認する。バーがlabelの付いた `progress` 要素か、名前のある `role="progressbar"` か、`aria-hidden="true"` で隠され値がそばにテキストで書かれていること、空の `role="status"` の領域がすでにページにあることを確かめる。
2. スクリーンリーダーを有効にし、キーボードでアップロードを始める。更新のたびではなく区切りごとにメッセージが読み上げられ、フォーカスはボタンに残る。
3. 途中でスクリーンリーダーをバーに移動する。名前と、描画と一致する値が読み上げられる。隠したバーの場合は、そばのテキストが描画と同じ値を示している。
4. アップロードを完了させ、次にブラウザーの開発者ツールでリクエストを止めて失敗させる。終了と失敗が、それぞれ言葉で読み上げられる。

OS、ブラウザーと支援技術のバージョン、ビルド、実施日、各手順の実際の結果を記録してください。アップロードの処理や進捗表示のコンポーネントを変更した際は、再度確認します。

## 制限

プログレスバーの伝え方はスクリーンリーダーによって異なります。値を割合で読むもの、ビープ音を鳴らすもの、利用者が移動したときにだけ読むものがあり、メーターの対応はさらに不安定です。このガイドは、一つのページの一つの処理のバーを扱っています。量の分からないスピナーや待ち時間は[読み上げられない読み込み中の表示](/ja/learn/loading-states)で、「下書きを保存しました」のような進み具合以外のメッセージは[画面に出ても読み上げられないステータスメッセージ](/ja/learn/status-messages)で扱っています。遅い場合や失敗する場合も含め、利用者が使うスクリーンリーダーで実際のアップロードを確認してください。

---

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