トピックを検索
読み上げられないスケルトンスクリーン
読み込み中は灰色の仮のカードを並べ、届いた商品と黙って差し替えるため、スクリーンリーダーには待っていることも、商品が届いたことも伝わらない
<div class="skeleton line"></div> 問題
スクリーンリーダーの利用者には、読み込み中は見出しの下が空のように聞こえ、商品が届いても知らせがない
生活雑貨の店のトップページに「新着アイテム」のセクションがあります。商品はページの表示後に別のリクエストで取得され、届くまでの間は灰色のカードが3枚並びます。写真の場所に四角が一つ、商品名と価格の場所に棒が2本あります。これがスケルトンスクリーンで、MUIのSkeleton、shadcn/uiのSkeleton、Bootstrapのプレースホルダーなど、多くのコンポーネントライブラリーに用意されています。1、2秒で商品が届き、灰色のカードと入れ替わります。画面が見える人には、まずレイアウトが見え、そこに中身が入っていく様子が分かります。
商品を取得している間に、スクリーンリーダーの利用者がページを上から読んだとします。聞こえるのは「新着アイテム、見出しレベル2」と、セクションの後ろにある「30日以内なら返品無料です。」です。灰色のカードは中身のない div なので、読むものがありません。商品を読み込んでいることは伝わらず、商品が届いても何も伝わりません。利用者はもうセクションを空だと思っているため、たまたま見出しまで戻らない限り、新着アイテムには出会えません。
もう一つの例では、すべてのプレースホルダーに aria-busy="true" と aria-live="polite" が付いています。React Loading Skeletonも、プレースホルダーを1つずつ包む span にこの2つを付けています。コードだけ見ると、待っていることを伝えていそうです。しかし伝わりません。プレースホルダーには相変わらず読むものがなく、商品が届くと、ライブリージョンはプレースホルダーごと取り除かれます。中身が変わったライブリージョンがないため、伝えるものがありません。
「新着アイテムを読み込み中…」や「新着アイテムを3件読み込みました」は、WCAGのいうステータスメッセージです。待っている状態や処理の結果を伝え、フォーカスは移しません。達成基準4.1.3は、こうしたメッセージをロールやプロパティで判別できるようにし、フォーカスを移さずに支援技術が伝えられることを求めています。スケルトンは待っている状態を灰色の形だけで示し、メッセージだと示すロールもプロパティもないため、見えない人には何も伝わりません。これはW3CがF103で示している失敗です。入れ替わりに表示される商品そのものは、達成基準の解説書が検索結果について述べているとおり、ステータスメッセージではありません。ステータスメッセージにあたるのは「新着アイテムを3件読み込みました」のような一文で、修正前にはそれがないため、待ち時間が終わったことも言葉では伝わりません。
2つ目の修正前は惜しいところまで来ていますが、どちらの属性も付ける場所が違います。aria-live を付けた要素はライブリージョンになり、ページにある間に中身が変われば、スクリーンリーダーが読み上げます。ところがスクリプトは、この領域の中身を変えずに、領域そのものを取り除きます。ライブリージョンは、初期設定では中身が取り除かれたことを伝えません。aria-busy="true" は、要素が更新中であり、更新が終わるまで伝えるのを待ってもよいと支援技術に知らせる属性です。メッセージではないため「読み込み中」とは言いません。しかもここでは、更新されずに捨てられるプレースホルダーに付いています。
スケルトンスクリーンは、商品一覧、フィード、ダッシュボード、検索結果など、ページの表示後に読み込まれる部分に使われます。後から読み込まれる部分こそ、スクリーンリーダーの利用者が見落としやすいところです。
10グループ中2グループに影響
ページの読み込みが終わるのを待つスキャンは、そろった一覧を見て合格にします。調べるころにはスケルトンは消えていますし、中身のない div はどのルールにも違反しません。スケルトンを探すaxeのルールもありません。EaseWebの診断は、読み込み中のページも見ています。スケルトンのクラスを持つプレースホルダー(MUIやReact Loading Skeletonが使う「skeleton」を含むクラス、shadcn/uiの data-slot="skeleton"、Bootstrapの placeholder-glow と placeholder-wave)が現れ、その外にライブリージョンがなければ、人が確認すべき項目として報告します。待っていることと届いたことが実際に読み上げられるかは、読み込み中にスクリーンリーダーで聞いて確かめるしかありません。スケルトンを使いやすくするようAIに頼むと、プレースホルダーそのものに aria-busy や aria-live を付けることがあります。これが2つ目の修正前です。
試してみる
灰色のカードが表示されている間に新着アイテムを読み、そのまま待って読み上げを聞いてみましょう
問題1: 灰色のカードが表示されている間に「ページを読む」を押します。下の欄に、スクリーンリーダーが上から読む内容と、その後に自分から読み上げる内容が出ます。そのまま商品が届くのを待ちます。
問題2: 同じカードの、すべてのプレースホルダーに aria-busy と aria-live を付けたものです。もう一度ページを読み、待ちます。
修正を適用します。 読み込み中にページを読み、そのまま待ちます。「もう一度読み込む」で最初からやり直せます。
問題あり 修正できますか?
新着アイテム
30日以内なら返品無料です。
スクリーンリーダーの読み上げ:
(灰色のカードが表示されている間に「ページを読む」を押してください)
修正方法
読み込み中であることと、届いたことを伝える
灰色の仮のカード
<h2>新着アイテム</h2><div id="arrivals" class="cards"> <div class="card"> <div class="skeleton photo"></div> <div class="skeleton line"></div> <div class="skeleton line short"></div> </div> …</div><p>30日以内なら返品無料です。</p>問題点2点
- 灰色のカードは中身のない
divなので、スクリーンリーダーは見出しからすぐ「30日以内なら返品無料です。」へ進み、商品を読み込んでいることは伝わりません。 - 商品はライブリージョンではない
divに書き込まれるため、届いたことが読み上げられません。
<h2>新着アイテム</h2><p id="arrivals-status" role="status" class="visually-hidden"></p><div id="arrivals" class="cards" aria-busy="true"> <div class="card" aria-hidden="true"> <div class="skeleton photo"></div> <div class="skeleton line"></div> <div class="skeleton line short"></div> </div> …</div><p>30日以内なら返品無料です。</p>修正の手順6ステップ
- 見出しの下、一覧の外に、
role="status"の空のpを置きます。こうすれば、スクリプトが書き込む時点で、ブラウザーはすでにこの領域を監視しています。visually-hiddenクラスで画面からは隠し、デザインは変えません。画面では、灰色のカードがすでに待っていることを示しています。 - 仮のカードそれぞれに
aria-hidden="true"を付けます。カードには伝えることがなく、ライブラリーがプレースホルダーに付けるライブリージョンやaria-busyも、一緒に黙ります。 - 一覧には、中身が入るまで
aria-busy="true"を付けます。この属性に従うスクリーンリーダーは、作りかけの一覧を伝えるのを待てます。属性そのものは何も読み上げません。 - リクエストを始めるときに、ステータスの行へ「新着アイテムを読み込み中…」と書き込みます。スクリーンリーダーは、読み上げ中の内容を終えてからこの文言を読みます。この時点でセクションを読むと、見出しの次にこの行が読まれます。
- すべての商品を一覧に入れたら、「新着アイテムを3件読み込みました。」と件数を書き込みます。リクエストが失敗したときは、灰色のカードを消し、代わりに「新着アイテムを読み込めませんでした。」と書き込みます。
- どちらの場合も、
aria-busyをfalseに戻します。
これで2つの修正前の問題はどちらも直り、4.1.3を満たします。待っていることも届いたことも、フォーカスを動かさずにステータスの領域で言葉にして伝えます。メッセージはカードごとではなく、一覧がそろったときに一度だけ書き込みます。
この方法を選ぶ理由:変化が落ち着いてから、一度だけ伝える
プレースホルダーには名前を付けず、隠します。 灰色のカードそれぞれに role="img" と「読み込み中」の aria-label を付けたり、カードごとに role="status" を付けたりすると、同じ言葉がカードの数だけ読まれ、12枚の一覧なら12回聞こえます。メッセージはセクション全体についてのものなので、一か所にまとめます。Bootstrap自身のプレースホルダーの例も、灰色のカードに aria-hidden="true" を付けており、プレースホルダーを中身と差し替え、更新を支援技術に知らせるには、JavaScriptが必要になる場合があると説明しています。
メッセージは、最後の項目がそろってから一度だけ。 複数のリクエストやストリーミングで、カードが1枚ずつ届く場合も、件数は最後の1枚の後に書き込みます。一覧そのものに aria-live を付けてはいけません。カードが追加されるたびに商品名と価格が読まれ、一つのメッセージではなく細切れの読み上げになります。
aria-busy はなくても構いません。 ARIAは、要素が更新中の間、支援技術が待つことを認めていますが、すべてのスクリーンリーダーが従うわけではありません。そのため、この修正は aria-busy がなくても成り立ちます。使う場合は、失敗も含めて、どの終わり方でも false に戻してください。この属性に従うスクリーンリーダーは、更新中のままの一覧を利用者に伝えないことがあります。
メッセージを画面に見せても構いません。 同じステータスの行から visually-hidden を外し、見出しの下に「新着アイテムを読み込み中…」、続いて「新着アイテム3件」と出せば、何が起きているかが誰にでも分かります。読み込みが遅いときに特に役立ちます。灰色のカードは、あと1秒で届くときも、止まってしまったときも、同じに見えるからです。
灰色のカードを残しません。 失敗したリクエストや、とても時間がかかるリクエストは、残ったプレースホルダーではなく、画面上の言葉で終わらせます。明滅やきらめきは合計5秒未満で数回にとどめ、その後はカードを止めます。動き続けるアニメーションは別の問題で、止まらずに動き続けるボタンやバッジのアニメーションで扱っています。
領域は、差し替える部分の外に置きます。 コンポーネントのフレームワークでは、スケルトンと中身が一つの条件の2つの分岐になっていることがよくあります(loading ? <Skeleton /> : <Grid />)。ステータスの行はどちらの分岐にも入れず、ページと一緒に表示します。メッセージと一緒に現れた領域は、読み上げられないことがよくあります。
フォーカスは動かしません。 商品が届くとき、利用者はほかの場所を読んでいるかもしれません。一覧へフォーカスを移すと、読んでいた場所から引き離してしまいます。絞り込みのボタンのように、ボタンを押して読み込みが始まる場合は、メッセージをそのボタンのそばで伝えます。読み上げられない読み込み中の表示で扱っています。
デザインの案
待っていることも、届いたことも言葉にする
読み込み中の「新着アイテム」のカードを描く4つの方法です。見本は画面に入ると読み込みを始め、「もう一度読み込む」で最初からやり直せます。3つは待っていることとその結果を一度ずつ言葉で伝えるので合格です。1つはカードに「読み込み中…」と書いていますが、不合格です。
デザインファイルで
- 読み込み中の言葉と、それに代わる言葉を、一字一句書く。たとえば「新着アイテムを読み込み中…」と「新着アイテム3件」。
- 読み込みに失敗したときの画面を描く。灰色のカードがあった場所に、何が起きたかを伝える文と「再読み込み」ボタンを置く。
- 灰色のカードは飾りだと書き添え、中に言葉を入れない。
- 灰色のカードが何回、1回何秒で明滅するかを書く。合計5秒未満にし、待ち時間が長いときの止まった状態も描く。
合格
新着アイテム
カードの上に添える読み込み中の一行 見出しの下の控えめな一行が「新着アイテムを読み込み中…」、続いて「新着アイテム3件」と伝えます。白地に#4b5563で7.56:1です。スクリーンリーダーが読むのもこの一行なので、見える人にも読み上げを聞く人にも同じ言葉が届き、時間がかかっているだけの読み込みが止まった読み込みと同じに見えることもなくなります。新着アイテム
1枚ずつ埋まっていくカード 灰色のカードが、届いた順に0.4秒おきに商品へ変わり、0.2秒かけて現れるので、ページが速く感じられます。読み上げる言葉は最後のカードを待って、「新着アイテムを3件読み込みました。」と一度だけ伝え、カードごとには伝えません。基本原則:変化が落ち着いてから、一度だけ伝える
新着アイテム
失敗をメッセージとして描いた読み込み リクエストが失敗すると、灰色のカードが「新着アイテムを読み込めませんでした。」という文と「再読み込み」ボタン(#1d4ed8に白で6.70:1)に変わり、同じ文が一度読み上げられます。灰色のカードを残したままでは、見える人にはまだ商品が届く途中のように見え、それ以外の人には何も伝わりません。
不合格
新着アイテム
読み込み中…読み込み中…読み込み中…カードごとに書いた「読み込み中…」WCAG 4.1.3 言葉は画面に3回、灰色の上に#4b5563で(6.10:1)書かれていますが、カードの一部なので、カードと一緒に消えてしまいます。たまたま行き当たったスクリーンリーダーはカードの数だけ読み、商品が届いたことは何も伝わりません。カードの外の一行なら、同じことを一度で伝えられます。
修正の確認
4項目、マウス不要
確認方法: スクリーンリーダー
OS、ブラウザーと支援技術のバージョン、ビルド、実施日、各手順の実際の結果を記録してください。読み込み表示のコンポーネントやデータの取得処理を変更した際は、再度確認します。
スクリーンリーダーでの role="status"、aria-busy="true"の読まれ方
制限
ライブリージョンと aria-busy の動きは、スクリーンリーダーやブラウザーによって異なります。ページそのものの読み込み中に書き込んだメッセージは、スクリーンリーダーが同時にページを読み始めると、遅れて読まれるか、読まれないことがあります。ステータスの行は読み上げ順の中に残るため、後からセクションに来た人にも「新着アイテムを読み込み中…」か件数が伝わります。このガイドでは、ページ内のあるセクションが届くまで、その代わりに表示するプレースホルダーを扱っています。ボタンを押した後のスピナーや読み込み中の文言は読み上げられない読み込み中の表示で、スクロールで読み込み続ける一覧はTabで近づくとフッターが遠ざかる無限スクロールで、新しい画面全体は読み上げられないSPAの画面遷移で扱っています。利用者が使うスクリーンリーダーで、実際のページを確認してください。
よくある問題を再現した学習用のサンプルです。実在のクライアントの診断結果ではありません。コードは出発点となる実装例のため、実際の製品でも確認してください。ここでは支援技術でのテスト結果は報告していません。
EaseWebのサービス
修正した問題を、次のリリースで再発させない
このガイドのような修正方法を、コーディングエージェントが従うルールと、CIのチェックに組み込みます。あわせて定期的に、キーボードとスクリーンリーダーで手動の再テストを行います。