# スクリーンリーダーが読み上げ続けるカウントダウンタイマー

Source: https://easeweb.dev/learn/countdown-timers
Topics: ライブ更新 > ライブのスコア、ティッカー、タイマーが変化のたびに読み上げない (WCAG 4.1.3, 2.2.2)
The fix: セールの終了時刻を time 要素で固定の時刻として示し、何も変わらず何も読み上げられないようにします。または、カウントダウンを残してライブリージョンから外し、変化のたびには読み上げられない role="timer" を付け、その前に「カウントダウンを止める」ボタンを置いて、刻む時間を終了時刻に置き換えられるようにします。
Test with: screen reader, keyboard
References: https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html https://www.w3.org/WAI/WCAG22/Techniques/general/G186 https://www.w3.org/TR/wai-aria-1.2/#timer https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/timer_role https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-live https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/time https://webaim.org/projects/screenreadersurvey10/

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

## 場面

リネンの店が秋のセールを開いていて、どの商品ページも「セール終了まで 02:14:09」というバナーで始まります。数字は1秒ずつ減っていきます。開発者は、セールの終わりが近いことをスクリーンリーダーの利用者にも伝えたいと考え、バナーに `aria-live="assertive"` を付けました。

スクリーンリーダーの利用者がティータオルのページを開き、説明を1行ずつ読みます。JAWS、NVDA、ナレーターでは、行は最後まで読まれ、そのあとに時計が続きます。「手織りのリネンのティータオル。洗いをかけてあり、最初から柔らかな手触りです。02:14:06」。次の行のあとには「02:14:03」。少し手を止めて考えていると、時間が1秒ごとに読まれ続けます。MacやiPadのVoiceOverでは、新しい時間が行を途中で切ります。「手織りのリネンの、02:14:08」と聞こえ、行の残りは読まれません。どちらでも、説明、価格、「カートに入れる」ボタンは、時間と時間の合間に読むことになります。数字の動きで気が散る人にも、止める手段がありません。バナーにはボタンがなく、ページを開いている限り数字は減り続けます。

## 確認できる問題

このバナーには、二つの問題があります。一つは4.1.3です。`aria-live="assertive"` の領域は、変化のたびにすぐ伝えるようスクリーンリーダーに求め、ARIAは待っていた内容を捨ててもよいとしています。Understanding 4.1.3は、ステータスメッセージを「作業を不必要に妨げない方法で」伝えるべきだとし、重要でも急ぎでもない内容に `role="alert"` や `aria-live="assertive"` を使うことを失敗例に挙げています。過ぎたばかりの1秒は、重要でも急ぎでもありません。ただし、スクリーンリーダーがすべて領域の求めどおりに動くわけではありません。2026年のリリースでは、JAWS、NVDA、ナレーターは assertive の領域を polite と同じように扱い、読んでいる文や行が終わってから読みました。割り込んだのは、macOSとiPadOSのVoiceOverです。つまり、変化はすべて読まれ、領域の求めに従うスクリーンリーダーでは、ページの読み上げを押しのけて読まれます。バナーはもともとステータスメッセージではありません。スクリプトが数字を一つ変えるだけなのに、領域がその変化を毎回の読み上げにしています。

もう一つは2.2.2です。カウントダウンは自動更新される情報です。Understanding 2.2.2の言葉では、あらかじめ決めた時間の間隔で更新される内容です。自動で始まり、商品と並んで表示されるため、WCAGは、一時停止、停止、非表示のいずれか、または更新の頻度を調整する手段を求めています。自動更新には、5秒以内なら許されるという例外もありません。Understanding 2.2.2は、こうした内容が「スクリーンリーダーにも問題を起こす」ことがあるとも述べています。この例では、ライブリージョンがまさにそれを引き起こしています。

## 利用者への影響

セールの間はどの商品ページにもバナーがあるため、時間の読み上げは店の中のどこへ行ってもついてきて、セールが終わるまで止まりません。スマートフォンやタブレットでは、スクリーンリーダーの利用者の多くがVoiceOverを使っています（WebAIMの2024年の調査では、モバイルの回答者の70.6%）。先のテストで、macOSとiPadOSで割り込んだスクリーンリーダーです。

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

スキャンは、最初からページにあり、文字の入ったライブリージョンを見て合格にします。その文字が毎秒変わることは、マークアップのどこにも書かれていません。この診断は、ページを操作する前の数秒間、ページを見ています。assertive の領域の中で文字が自動で3回以上変われば問題として報告し、自動で変わり続ける文字は、どの領域にあっても、人が確認する対象として挙げます。ただし、ページの別の場所にある停止ボタンがその文字を止めるかどうかは分かりません。カウントダウンをアクセシブルにするよう頼まれたAIアシスタントは、`aria-live` を付けることがあり、このバナーもそうして生まれました。`role="timer"` と一緒に `aria-live="assertive"` を付けて、割り込みを元に戻してしまう例もあります。カウントダウンが動いている間にページを聞いてみて、初めて問題に気づきます。どう聞こえるかは、スクリーンリーダーによって違います。

## 修正前：急ぎ用のライブリージョンにあるカウントダウン

```html
<p class="sale-banner" aria-live="assertive">
  セール終了まで <span id="sale-clock">02:14:09</span>
</p>
```

```js
const end = new Date('2026-10-31T23:59:00Z');
const clock = document.querySelector('#sale-clock');

function tick() {
  clock.textContent = timeLeft(end); // "02:14:08"
}

setInterval(tick, 1000);
```

スクリプトは数字を毎秒変え、その領域は assertive です。そのため、変化が読み上げられます。JAWS、NVDA、ナレーターでは行のたびに、VoiceOverでは行を押しのけて読まれます。ページには、それを止めるものが何もありません。

## 修正後：一度だけ書く終了時刻

```html
<p class="sale-banner">
  セールは <time id="sale-end" datetime="2026-10-31T23:59:00Z">10月31日 23:59</time> まで
</p>
```

```js
const end = new Date('2026-10-31T23:59:00Z');
const endText = document.querySelector('#sale-end');

// 訪問者のタイムゾーンで一度だけ書き、そのあとは何も変えない。
endText.textContent = end.toLocaleString([], { day: 'numeric', month: 'long', hour: '2-digit', minute: '2-digit' });
```

ページを読み込んだあとは何も変わらないため、読み上げるものも、止めるものもありません。2.2.2が求めることもなくなります。バナーはライブリージョンではないので、スクリーンリーダーは利用者がそこまで来たときに、ほかの行と同じように「セールは10月31日 23:59まで」と一度読みます。`time` 要素の `datetime` が正確な時点を持ち、スクリプトが見える文字を訪問者のタイムゾーンで書き直すため、時差を計算する必要もありません。代わりに、数字が刻む動きはなくなります。店によっては、セールの切迫感を出すために使っている動きです。

## 修正後：止めるボタンのある、静かなカウントダウン

```html
<p class="sale-banner">
  <button type="button" id="sale-stop">カウントダウンを止める</button>
  <span id="sale-left">セール終了まで <span id="sale-clock" role="timer">02:14:09</span></span>
  <span id="sale-end" hidden>セールは <time datetime="2026-10-31T23:59:00Z">10月31日 23:59</time> まで</span>
</p>
```

```js
const end = new Date('2026-10-31T23:59:00Z');
const clock = document.querySelector('#sale-clock');
const left = document.querySelector('#sale-left');
const endText = document.querySelector('#sale-end');
const button = document.querySelector('#sale-stop');

function tick() {
  clock.textContent = timeLeft(end); // "02:14:08"
}

let timer = setInterval(tick, 1000);

// 止めると、すぐに古くなる時間ではなく、終了時刻を表示する。
button.addEventListener('click', () => {
  const stopping = !left.hidden;
  clearInterval(timer);
  if (!stopping) {
    tick();
    timer = setInterval(tick, 1000);
  }
  left.hidden = stopping;
  endText.hidden = !stopping;
  button.textContent = stopping ? 'カウントダウンを表示' : 'カウントダウンを止める';
});
```

カウントダウンは今も刻みますが、バナーはもうライブリージョンではありません。時計に付けた `role="timer"` は、`aria-live` が既定で `off` のライブリージョンです。そのため、変化してもスクリーンリーダーは何も言わず、利用者がそこまで来たときに残り時間を読みます。「カウントダウンを止める」ボタンは時計の前にあるため、キーボードでは先に届きます。押すと更新が止まり、代わりに終了時刻が表示されます。一時停止したカウントダウンは、古くなった時間を示し続けます。Understanding 2.2.2も、一時停止後のオークションのタイマーは誤解を招く情報を示すと述べています。終了時刻なら、正しいままです。もう一度押すとカウントダウンに戻ります。店が求めた切迫感は残りますが、ボタンを見つけて止めるまでは、誰にとっても刻み続けます。

## どの方法を選ぶか

どちらも4.1.3と2.2.2を満たします。終了時刻は作る手間がかからず、誰にとっても一度読まれるだけで、止めるものもありませんが、数字が刻む動きはなくなります。静かなカウントダウンは刻む動きを残せますが、ボタンと少し多いスクリプトが必要で、ボタンを見つけるまでは誰にとってもバナーが動き続けます。刻む動きがなくても困らない店なら終了時刻を、刻む動きがセールの売り方の一部なら、ボタン付きの静かなカウントダウンを選んでください。

## 実装の判断

**politeにしても直りません。** 領域を `aria-live="polite"` に変えると、VoiceOverが行の途中に割り込むことはなくなります。それでも、どのスクリーンリーダーも行を読むたびに新しい時間を読むため、ページは時間と時間の合間に読むことになります。JAWS、NVDA、ナレーターが assertive の領域ですでにしていることと同じです。Understanding 4.1.3は、ライブリージョンやアラートを使うと、アプリケーションがスクリーンリーダーの利用者にとって「おしゃべり」になりすぎるおそれがあると警告しています。カウントダウンへの答えは、assertive の場合と同じで、時計をライブリージョンに入れないことです。

**`role="timer"` で静かにします。** timer は `aria-live` が既定で `off` のライブリージョンで、変化は読み上げられません。`aria-live` を付け足さないでください。`role="timer" aria-live="assertive"` は修正前に戻るだけです。`aria-live` を外し、役割を付けないままでも読み上げは止まります。役割は、変わり続ける数字が何なのかを支援技術に伝えます。中身の前に名前を読むスクリーンリーダーもあります。

**大事なら、区切りに一度だけ伝えます。** セールの終わりが近いことを聞く必要は、ほとんどありません。数分だけ確保される予約のように必要な場合は、区切りのよい時点（「残り10分」）に一度だけ `role="status"` の領域へメッセージを書き込み、時計は静かなままにします。MDNのtimerの例は、残り10秒でタイマー自体の役割を `alert` に変えていますが、別のステータス領域を使えば、時計をライブリージョンにしないまま、区切りを一度だけ伝えられます。利用者自身の作業にかかる制限時間は別の問題で、[警告も延長もないセッションタイムアウト](/ja/learn/time-limits)で扱っています。

**選んだ状態を保ちます。** 止めた状態を `sessionStorage` などに保存し、訪問者が別の商品ページを開くたびにバナーが動き出さないようにします。

**非表示でも構いません。** 2.2.2は、一時停止、停止、非表示のどれでもよいとしています。バナーを消す「閉じる」ボタンでも満たしますが、セールの終わりの情報も一緒に消えるため、ページの別の場所に終了時刻を示してください。

**スコア、フィード、価格。** 自動で変わるものがある場所では、同じ問題が起き、修正の考え方も同じです。

- ライブのスコア：試合の時計は `role="timer"` にするかライブリージョンに入れず、スコアは変わったときだけ `role="status"` の領域に書き込み（「ゴール：Rovers 2、City 1」）、更新を一時停止するボタンを付ける。
- ライブブログやニュースのフィード：新しい項目は polite の `role="log"` の領域に追加し、「更新を一時停止」ボタンを付ける。自動で伸びていくフィードも、自動更新される内容にあたる。
- 更新される価格や在庫数：数字をライブリージョンに入れず、更新を止めるか頻度を下げるボタンや設定を付ける。

**動きと緊急度は別のトピックです。** 横に流れるティッカーは動くコンテンツで、修正は[一時停止できない、流れる文字、カルーセル、アニメーション](/ja/learn/moving-content)で扱っています。本当に急ぎのメッセージには `role="alert"` が正しく、その使い分けは[定型の通知でもVoiceOverの読み上げを遮るアラート](/ja/learn/urgent-alerts)で扱っています。

## 修正の確認

1. DOMでバナーを確認し、時計にもその周りにも `aria-live`、`role="alert"`、`role="status"` がなく、動いている時計には `role="timer"` があることを確かめる。
2. カウントダウンが動いている間に、スクリーンリーダーで商品ページを30秒間1行ずつ読む。行と行の間に時間が読まれず、途中で切れる行もない。行を途中で切るのはVoiceOverだけなので、JAWS、NVDA、ナレーターのどれかに加えて、VoiceOverでも確認する。
3. スクリーンリーダーでバナーへ移動する。残り時間か終了時刻が、「セール終了まで」や「セールは」と一緒に一度読まれる。
4. キーボードだけで「カウントダウンを止める」を押し、読み進めてから戻る。バナーは終了時刻を示したままで、再び動き出していない。終了時刻の版では、読み込み後にバナーの何も変わらないことを確かめる。

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

## 制限

ライブリージョンの動きは、スクリーンリーダーやブラウザーによって、またバージョンによっても異なります。このガイドの記述は、2026年時点の各リリースでの動きです。Understanding 4.1.3も、利用者の設定によっては、支援技術がステータスメッセージを遅らせたり、抑えたり、形を変えたりすることがあると述べています。このガイドは、一つのページで、ほかの内容と並ぶカウントダウンを扱っています。オークションの時計のように、更新が活動に欠かせないかどうかはチームの判断で、その場合2.2.2は止める手段を求めません。それでも、時計が毎秒読み上げる必要はありません。利用者が使うスクリーンリーダーで確認してください。

---

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