📣

スクリーンリーダーを起動せずにコンテンツを読み上げたい

2024/11/11に公開

はじめに

「Webページに文章書いてあるんだから読めば誰だってわかるでしょ。」と思ったことはありますか?顕在的に思ったことがないとしても、潜在的にそう思って実装してしまうことは多いのではないでしょうか。

そこに文章が書いてあっても、いろいろな事情でそれが読めない人は存在します。

例えば、視覚に困難をお持ちの方。全く見えないことや文字を拡大しても読めないことが想定できます。また、認知能力に困難をお持ちの方。漢字が読めないことが想定できます。

世界に目を向けると識字率の問題も出てきます。インターネットにアクセスできても文字を読むことが困難な人は存在します。

そういった人たちにもコンテンツを届けるにはどうしたら良いでしょうか。

その一つの手段として「コンテンツの読み上げ機能」があると思います。スクリーンリーダーを使用すればそれが実現可能ですが、使い慣れてない人にとっては操作自体が困難になってしまいます。

本記事は、スクリーンリーダーを使用しない読み上げ機能を実装しようという記事です。

対象読者

  • 読み上げ機能を実装したい人
  • アクセシビリティに興味がある人

実装環境

"dependencies": {
  "next": "15.0.3",
  "react": "^18.3.1",
  "react-dom": "^18.3.1",
  "zustand": "^5.0.1"
}

読み上げまでの大まかな流れ

スクリーンリーダーを起動せずにコンテンツを読み上げるまでの大まかな流れは下記のとおりです。

  1. 読み上げる要素の取得
  2. 読み上げの設定
  3. 読み上げる

実装例を見ながら具体的に確認していきましょう。

読み上げる要素の取得

ref経由で取得します。

読み上げたいテキストの親にrefを付与し、innerText経由で取得することができます。
下記の例だとref.current.innerTextは、読み上げたい文章をここにとなります。

export function Contents() {
  const ref = useRef<HTMLParagraphElement | null>(null);
	
  function getTextForSpeech() {
    if(!ref.current) return;
		
    return ref.current.innerText;
  }

  return <p ref={ref}>読み上げたい文章をここに</p>
}

タグが入れ子になっている場合はこんな風にrefを付与します。
下記の例だとref.current.innerTextは、タイトル\n\n読み上げたい文章をここにとなります。

return (
  <div ref={ref}>
    <h1>タイトル</h1>
    <p>読み上げたい文章をここに</p>
  </div>
)

innerTextではなくtextContentでも取得することができますが、結果が異なるので注意してください。MDNに両者の違いが説明されているので引用します。

  • textContent は、<script> と <style> 要素を含む、すべての要素の中身を取得します。一方、innerText は「人間が読める」要素のみを示します。
  • textContent はノード内のすべての要素を返します。一方、innerText はスタイルを反映し、「非表示」の要素のテキストは返しません。
    • もっと言えば、innerText は CSS のスタイルを考慮するので、innerText の値を読み取ると最新の計算されたスタイルを保証するために再フローを起動します。(再フローは計算が重いので、可能であれば避けるべきです。)

https://developer.mozilla.org/ja/docs/Web/API/Node/textContent#innertext_との違い

読み上げの設定

読み上げたい要素を取得できたら、次は読み上げる準備です。

Web Speech APIが提供しているSpeechSynthesisUtteranceクラスを使用します。

初期化する際に、引数へ読み上げたいテキスト情報を渡してあげます。インスタンスには言語を設定するlangと読み上げ速度を設定するrateというプロパティが存在するので読み上げたい内容に合わせて設定します。

function prepareSpeech() {
  if (!ref.current) return;
  // 初期化
  const utterance = new SpeechSynthesisUtterance(getTextForSpeech(ref.current));
  // 言語は日本語を指定
  utterance.lang = 'ja-JP';
  // 読み上げ速度は通常
  utterance.rate = 1;
    
  return utterance;
}

他にもpitchvoiceなども設定できるので気になる人はMDNを確認してみてください。

https://developer.mozilla.org/ja/docs/Web/API/SpeechSynthesisUtterance

読み上げる

読み上げるには、WindowオブジェクトのspeechSynthesisプロパティが持つspeakメソッドに前ステップで初期化したSpeechSynthesisUtteranceインスタンスを渡すことで実現します。

function startSpeech() {
  const utterance = prepareSpeech();
  if(!utterance) return;
  // 読み上げる
  window.speechSynthesis.speak(utterance);
}

このメソッドをbuttonタグのonClick等に接続することで、ユーザーがボタンをクリックした時に読み上げることができます。

ただ、この実装だと連続でボタンをクリックされた際に、2回目の読み上げが1回目の読み上げが終わるまで開始されないので、直前でcancelメソッドを呼ぶことにしました。

function startSpeech() {
  const utterance = prepareSpeech();
  if(!utterance) return;
  // 直前の読み上げを停止する
  window.speechSynthesis.cancel();
  // 読み上げる
  window.speechSynthesis.speak(utterance);
}

https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis/speak

https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis/cancel

完成

以上で読み上げ機能は完成です!

おわりに

いかがでしたでしょうか。

「Webページに文章書いてあるんだから読めば誰だってわかるでしょ。」だとほとんどのユーザーは理解できる内容かもしれませんが、すべてのユーザーが理解できるようにはなりません。

一人でも多くの人が理解できるように、目以外でも理解できる内容になっているかということを意識していくことも、プロダクトを開発するエンジニアとして忘れてはいけないことだと考えるキッカケになりました。

今後もすべてのユーザーのためになる技術をたくさん学んでいこうと思います。

読み上げ機能を搭載したアンケート回答アプリのデモを作ってみたので興味がある人は是非覗いてみてください。

デモアプリ

https://dm-speech-contents.vercel.app/

ソースコード

https://github.com/hakushun/dm_speech-contents

Discussion