close up on woman making heart sign
Photo by RDNE Stock project on <a href="https://www.pexels.com/photo/close-up-on-woman-making-heart-sign-10029254/" rel="nofollow">Pexels.com</a>

【実装解説】WCAG 2.2に準拠した「音声読み上げ」の操作方法:要件、UI設計、コード例、チェックリストまで

先に要点(インバーテッドピラミッド)

  • 必須の規格項目:自動再生音声には停止・一時停止・音量の操作を必ず用意(1.4.2 Audio Control)。視覚的な動きの自動更新は停止/非表示を提供(2.2.2 Pause, Stop, Hide)。ボタンやスライダーなどUI部品は名前・役割・値を機械可読に(4.1.2 Name, Role, Value)、状態メッセージはフォーカス移動なしで通知4.1.3 Status Messages)。
  • 2.2で新たに押さえる点フォーカスが隠れない(2.4.11 AA)とフォーカスの見え方(2.4.13 AAA)で、読み上げコントロールの可視フォーカスを強化。ターゲット最小サイズ(2.5.8 AA, 24×24 CSS px)とドラッグ操作の代替(2.5.7 AA)も適用範囲です。
  • UI原則<button>に本物のボタン<label>で明示ラベルaria-pressed/aria-liveで状態・通知。キーボード操作音量/速度の調整、一時停止/停止を確実に。
  • サンプル実装:Web Speech API(SpeechSynthesis)でTTSを実装し、1.4.2/4.1.2/4.1.3/2.5.8に沿ったコントローラを提供。ブラウザ差はフォールバックで吸収します。

1. まず「WCAG 2.2で必要なこと」を地図化しましょう

音声読み上げの操作は、WCAGでは複数の達成基準にまたがります。最低限、次の5本柱を押さえると実装迷子になりません。

  1. 1.4.2 Audio Control(A)
    ページで3秒を超えて自動再生される音声があるなら、停止・一時停止、またはシステム音量と独立の音量調整を提供すること。読み上げUI(TTS)を提供する場合も、まとめて明確な操作を出すのが安全です。

  2. 2.2.2 Pause, Stop, Hide(A)
    自動で動く・更新される領域は停止/非表示を可能にします。読み上げ中にテキストが自動更新されるウィジェットがあるなら、更新の一時停止ライブ領域の制御が必要です。

  3. 4.1.2 Name, Role, Value(A)
    ボタン・スライダー・セレクト等にプログラムで取得できる名前(accName)と役割、値や状態を持たせます(<button>, <input type="range">, aria-pressedなど)。

  4. 4.1.3 Status Messages(AA)
    「読み上げ開始」「一時停止」などの状態通知は、フォーカスを動かさずにスクリーンリーダーへ伝えます(aria-live="polite" など)。

  5. 2.5.7/2.5.8(AA)と2.4.11/2.4.13

  • ドラッグ不要な操作も用意(2.5.7)。読み上げのシーククリック/キー操作でも可能に。
  • ターゲット最小サイズ24×24 CSS px(例外あり)。小さすぎる再生/停止ボタンはNGです(2.5.8)。
  • フォーカスが隠れない2.4.11)+見え方の強化2.4.13AAA)。固定ヘッダーやダイアログにフォーカスが埋没しない設計・スタイルを。

なお、2.2で新設された基準の一覧はW3C公式の「What’s new」で確認できます(4.1.1 Parsingは削除など)。


2. UI設計の原則:スクリーンリーダー・キーボード・ポインタの三立て

**基本は「ネイティブ要素+見た目」**です。<div role="button"> より <button> を。ARIAは足りないときだけ使います。WAI-ARIAのボタン・パターンが良い指針になります。

  • 構成(推奨)

    • 再生/一時停止(トグル)<button aria-pressed="false">
    • 停止<button>(明示的に「最初から止める」)
    • 音量<input type="range" min="0" max="1" step="0.1"><label>
    • 速度<input type="range" min="0.5" max="2" step="0.1"><label>
    • 声種<select><label>(端末の音声リストから)
    • 状態通知<div aria-live="polite">(「再生中 00:35」など)
  • 可視フォーカス

    • フォーカスリングは隠さない/潰さない。必要に応じて太さ・コントラスト3:1以上を確保(2.4.13 AAAの考え方)。固定ヘッダー・スティッキー要素でフォーカスが隠れないように(2.4.11)。
  • ターゲットサイズ

    • 最低 24×24 CSS px 相当を確保。密集する場合は十分なスペースで誤タップを防止(2.5.8)。
  • 操作代替(ドラッグ不要)

    • シークバーはクリック/矢印キーで移動できる実装に(2.5.7)。

3. コードサンプル:Web Speech APIで作る「読み上げコントローラ」

目的:1.4.2/2.2.2/4.1.2/4.1.3/2.5.8を押さえた最小構成。
備考:Web Speech API(SpeechSynthesis)ブラウザ互換に差があります。未対応環境のフォールバック(音声なしでもテキストが読める、外部TTSに渡す等)を用意してください。

<section aria-labelledby="tts-title">
  <h2 id="tts-title">本文の音声読み上げ</h2>

  <!-- コントローラ(24px以上のクリック領域+可視フォーカス) -->
  <div role="group" aria-labelledby="controls-title" class="tts-controls">
    <h3 id="controls-title" class="sr-only">読み上げコントロール</h3>

    <button id="playPauseBtn" aria-pressed="false" aria-describedby="playPauseHint">
      ▶︎ 再生
    </button>
    <span id="playPauseHint" class="sr-only">もう一度押すと一時停止</span>

    <button id="stopBtn">■ 停止</button>

    <label for="volume">音量</label>
    <input id="volume" type="range" min="0" max="1" step="0.1" value="1" />

    <label for="rate">速度</label>
    <input id="rate" type="range" min="0.5" max="2" step="0.1" value="1" />

    <label for="voice">声</label>
    <select id="voice" aria-describedby="voiceNote"></select>
    <p id="voiceNote" class="sr-only">端末にある音声から選択</p>
  </div>

  <!-- 状態通知(フォーカス移動なしで読み上げ) -->
  <p id="status" aria-live="polite" class="visually-noticeable">待機中</p>

  <!-- 読み上げ対象 -->
  <article id="readTarget">
    <h3>サンプル本文</h3>
    <p>ここに読み上げたいテキストが入ります。キーボードでも操作できます。</p>
  </article>
</section>

<style>
  .sr-only { position:absolute; left:-9999px; }
  .tts-controls button, .tts-controls input, .tts-controls select {
    min-width: 48px; min-height: 32px; /* 24×24以上を確保 */
    margin: .25rem;
  }
  :focus { outline: 3px solid #1a73e8; outline-offset: 2px; } /* 視認しやすいフォーカス */
</style>

<script>
  const textEl = document.getElementById('readTarget');
  const statusEl = document.getElementById('status');
  const playPauseBtn = document.getElementById('playPauseBtn');
  const stopBtn = document.getElementById('stopBtn');
  const volume = document.getElementById('volume');
  const rate = document.getElementById('rate');
  const voiceSel = document.getElementById('voice');

  const synth = window.speechSynthesis;
  let utterance = null;
  let paused = false;

  // 音声一覧の取得(環境依存)
  function loadVoices() {
    const voices = synth.getVoices();
    voiceSel.innerHTML = '';
    voices.forEach((v, i) => {
      const opt = document.createElement('option');
      opt.value = i;
      opt.textContent = `${v.name} (${v.lang})`;
      voiceSel.appendChild(opt);
    });
  }
  loadVoices();
  if (typeof speechSynthesis !== 'undefined') {
    speechSynthesis.onvoiceschanged = loadVoices;
  }

  function announce(msg) {
    statusEl.textContent = msg; // 4.1.3: フォーカス移動なしの通知
  }

  function createUtterance() {
    const u = new SpeechSynthesisUtterance(textEl.innerText.trim());
    const voices = synth.getVoices();
    if (voices[voiceSel.value]) u.voice = voices[voiceSel.value];
    u.volume = parseFloat(volume.value);
    u.rate = parseFloat(rate.value);

    u.onstart = () => announce('読み上げを開始しました');
    u.onpause = () => announce('一時停止しました');
    u.onresume = () => announce('再開しました');
    u.onend = () => {
      playPauseBtn.setAttribute('aria-pressed', 'false');
      playPauseBtn.textContent = '▶︎ 再生';
      paused = false;
      announce('読み上げを終了しました');
    };
    return u;
  }

  playPauseBtn.addEventListener('click', () => {
    const pressed = playPauseBtn.getAttribute('aria-pressed') === 'true';

    // 既に発話中
    if (synth.speaking && !paused) {
      synth.pause(); paused = true;
      playPauseBtn.textContent = '⏸ 再開';
      playPauseBtn.setAttribute('aria-pressed', 'true');
      return;
    }
    // 一時停止からの再開
    if (paused) {
      synth.resume(); paused = false;
      playPauseBtn.textContent = '⏯ 一時停止';
      playPauseBtn.setAttribute('aria-pressed', 'true');
      return;
    }
    // 新規開始
    utterance = createUtterance();
    synth.cancel(); // 競合をクリア(2.2.2の意図にも合致)
    synth.speak(utterance);
    playPauseBtn.textContent = '⏯ 一時停止';
    playPauseBtn.setAttribute('aria-pressed', 'true');
  });

  stopBtn.addEventListener('click', () => {
    synth.cancel();
    paused = false;
    playPauseBtn.setAttribute('aria-pressed', 'false');
    playPauseBtn.textContent = '▶︎ 再生';
    announce('停止しました');
  });

  [volume, rate, voiceSel].forEach(el => {
    el.addEventListener('input', () => {
      if (synth.speaking) {
        // 反映のためにいったん停止→再開(文中の更新は実装依存)
        synth.cancel();
        utterance = createUtterance();
        synth.speak(utterance);
      }
    });
  });
</script>

このサンプルが満たすポイント

  • 1.4.2停止/一時停止/音量の操作を提供。自動再生はせず、ユーザー起動時にのみ再生。
  • 2.2.2cancel()競合する発話を停止し、望まない自動更新の中断を担保。
  • 4.1.2<button>, <label>, <input type="range">, <select>名前・役割・値が機械可読。トグルはaria-pressedで状態を露出。
  • 4.1.3aria-live="polite"で**「開始/一時停止/終了」フォーカス移動なし**に通知。
  • 2.5.8:最小ターゲットサイズ24×24 CSS px以上を確保(CSS)。
  • 2.5.7:シークをドラッグ以外(クリック・キー)で実装可能な設計方針。
  • 2.4.11/2.4.13可視フォーカスを太さ・コントラストで明確化し、隠れないように配慮。

補足:ブラウザによってはSpeechSynthesisの挙動・音声リストの扱いが異なります。MDNの互換表を参照し、未対応時は読み上げUIを非表示/グレイアウトするなどのフォールバックを。


4. 「自動再生音声」や「BGM」があるサイトでの注意点

  • 自動再生の抑制:初期表示でBGMやナレーションが3秒を超えて鳴る設計は避けましょう。必要な場合は確実に停止・一時停止・音量を提供します(1.4.2)。スクリーンリーダー利用者にとって、音声の重なりは操作不能の原因になります。
  • 動きの停止(2.2.2)字幕テロップやライブティッカーなど自動で動く領域停止/非表示ができるUIを。読み上げ中に自動更新が割り込まないようライブ領域の粒度を見直します。

5. よく使うARIAと“落とし穴”

  • aria-liveの粒度:すべてをassertiveにすると情報洪水になります。読み上げの開始/停止politeで十分なことが多いです(4.1.3)。
  • 偽ボタン問題<div role="button">に頼るとキーボード/フォーカス/既定の可視リングが欠けがち。**ネイティブ<button>**を基本に。
  • ラベルの欠落<label for>aria-label がないと4.1.2違反に直結。音量/速度/声種は必ず見えるラベルを。
  • フォーカスの視認性不足:サイトデザインでフォーカスリングを消すのは禁物。色コントラストや太さで見やすく(2.4.13 AAAの趣旨)。

6. 受け入れテスト(手動+スクリーンリーダー)

キーボード

  • Tab/Shift+Tabすべての操作部品に到達できる。
  • Enter/Space再生/一時停止/停止が動く。
  • スライダーは矢印/PageUp/Down/Home/Endで調整できる(ブラウザ標準)。

スクリーンリーダー

  • NVDA/JAWS/VoiceOverで、「再生(ボタン)」「現在:一時停止」等の名前・役割・状態が正しく読まれる(4.1.2)。
  • 再生・停止時に**「読み上げを開始/停止」などのステータスフォーカス移動なし**でアナウンスされる(4.1.3)。

見た目・操作

  • フォーカスリングが常に視認でき、固定ヘッダー等で隠れない2.4.11)。
  • ボタンのクリック領域が24×24 CSS px以上2.5.8)。
  • ドラッグ操作が必須になっていない2.5.7)。

7. 実務チェックリスト(そのまま使える)

  • [ ] 自動再生なし。やむを得ず自動再生する音声は停止/一時停止/独立音量を提供(1.4.2)。
  • [ ] 読み上げの開始/停止ボタンで提供。**トグルはaria-pressed**で状態露出(4.1.2)。
  • [ ] 状態メッセージは**aria-live**で通知し、フォーカスを動かさない(4.1.3)。
  • [ ] ターゲットサイズ24×24 CSS px以上(2.5.8)。
  • [ ] ドラッグの代替操作(クリック/キー)を用意(2.5.7)。
  • [ ] フォーカスが隠れない視認できる(2.4.11/2.4.13)。
  • [ ] 更新/動き停止/非表示が可能(2.2.2)。

8. 対象読者と効果(具体)

  • Web/フロントエンド実装者ネイティブ要素+最小限ARIAで、テスト負荷と不具合を減らせます。2.5.8/2.4.11はCSSだけでも対応できる箇所が多く、工数対効果が高いです。
  • デザイナーフォーカスの見え方(2.4.13 AAA)やターゲットサイズ(2.5.8 AA)をデザインガイドに織り込み、再発注コストを抑制。
  • プロダクト/CS:自動再生・状態通知が適切になると離脱と問い合わせが減ります。特にBGM/ナレーションのある教育・観光サイトは1.4.2対応が効果的。
  • 公共・教育機関操作方法の明確化とステータス通知で、スクリーンリーダー利用者の利用体験が安定。審査・監査での指摘も減らせます(4.1.2/4.1.3)。

9. まとめ:ネイティブ要素+最小限ARIAで、“やさしい読み上げUI”に

  • 1.4.2/2.2.2音と動きのコントロールを確実に。4.1.2/4.1.3状態と名前を機械可読に。新設の2.5.8/2.5.7/2.4.11触りやすく/見つけやすく/隠れないUIに。
  • 実装はシンプルに<button><label>aria-liveフォーカスの見え方最小ターゲットサイズをCSSで担保すれば、WCAG 2.2準拠の大半は固まります。
  • 次の一歩:自サイトの音声・動きを棚卸し→上のチェックリストで差分を洗い出し→サンプルコードをベースに導入。今日から、ひとつずつ整えていきましょう。

参考(主要ガイド)

  • WCAG 2.2 本体2.2の変更点
  • 1.4.2 Audio Control(理解文書)2.2.2 Pause, Stop, Hide
  • 4.1.2 Name, Role, Value4.1.3 Status Messages
  • 2.5.8 Target Size(最低24×24 CSS px)2.5.7 Dragging Movements
  • 2.4.11 Focus Not Obscured(Minimum)2.4.13 Focus Appearance
  • WAI-ARIA ボタン・パターン

投稿者 greeden

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)