これはなに
h1〜h6の中で<br>を使ってレスポンシブ対応の改行を実装していたところ、VoiceOverが同じ見出しの中を移動するたびに「見出し レベルN」を繰り返し読み上げる問題に遭遇しました。
この記事では、なぜそうなるのかを整理しつつ、<br>とdisplay: blockにした<span>それぞれの対策をまとめます。
動作確認環境
- ブラウザ:Chorome
- 読み上げ支援技術:VoiceOver(macOS)
問題を再現する
たとえば、画面幅の大きいデスクトップではテキストを1行で、画面幅の狭いモバイルでは途中で折り返したいときに、こういった実装をすることがあると思います。
<h2>
エンジニアコミュニティで
<br class="mobile-only" />
技術力を発信しよう
</h2>
/* モバイルのみ表示してbrを有効化 */
.mobile-only {
display: none;
}
@media (max-width: 768px) {
.mobile-only {
display: inline;
}
}
視覚的には問題ないのですが、「VoiceOver + VO キー + ← `→」でテキストを読み進めると、次のような読み上げになります。
| 操作 | 読み上げ |
|---|---|
見出しに移動する(VO + →) |
「エンジニアコミュニティで、見出し レベル2」 |
さらに進む(VO + →) |
「技術力を発信しよう、見出し レベル2」 |
<h2>の中にいるはずなのに、「見出し レベル2」が2回読み上げられます。
なぜ繰り返されるのか
<br>の場合
VoiceOverはアクセシビリティツリーを辿って読み上げを行います。
SafariのAX実装では、<br>を境界としてその前後のテキストを 別々の静的テキストノード(AXStatic Text) として扱います。
このとき、分割された各ノードが親である<h2>のロール(見出し)とレベル(2)を
継承するため、VO + → でノードを移動するたびに「見出し レベル2」が再度アナウンスされてしまいます。
<br>を1個入れると2回、2個入れると3回と、<br>の数だけ繰り返されます。
display: blockにした<span>の場合
見た目上の改行をCSSで制御するために、<span>をdisplay: blockにするアプローチも同様の問題が起きます。
<h2>
<span class="block-on-mobile">エンジニアコミュニティで</span>
技術力を発信しよう
</h2>
@media (max-width: 768px) {
.block-on-mobile {
display: block;
}
}
display: blockになった<span>はブロックボックスを形成し、その前後でテキスト
のノードが分割されます。モバイル幅ではVoiceOverが「見出し レベル2」を繰り返してしまいます。
対策① aria-labelで上書きする
視覚的なマークアップ(<br>や<span>)をそのままに、VoiceOverに対しては見出
し全体を1つのテキストとして読ませる方法です。
aria-labelを見出し要素に付与すると、中のDOMツリーの読み上げに依存せず、aria -labelの値を見出し名として使います。
<h2 aria-label="エンジニアコミュニティで技術力を発信しよう">
エンジニアコミュニティで
<br class="mobile-only" />
技術力を発信しよう
</h2>
| 操作 | 読み上げ |
|---|---|
見出しに移動する(VO + →) |
「エンジニアコミュニティで技術力を発信しよう、見出し レベル2」 |
さらに進む(VO + →) |
次の要素へ移動 |
「見出し レベル2」が1回だけ読み上げられるようになります。
display: blockな<span>の場合も同様です。
<h2 aria-label="エンジニアコミュニティで技術力を発信しよう">
<span class="block-on-mobile">エンジニアコミュニティで</span>
技術力を発信しよう
</h2>
注意点
aria-labelを使う場合、視覚的なテキストとaria-labelの内容が一致しているこ
とを確認してください。一致していないと、スクリーンリーダーユーザーと視覚的な
ユーザーで異なる情報が伝わってしまいます。
また、テキストが更新される場合はaria-labelも合わせて更新する必要があります。
ロケールによって文章が変わる場合はとくに注意が必要です。
対策② CSSで改行する(根本対処)
そもそも<br>を使わず、見た目上の折り返しをCSSだけで制御する方法です。
white-space: pre-line を使う
改行コード(\n)をCSSで折り返しとして扱います。
<h2 class="heading">エンジニアコミュニティで 技術力を発信しよう</h2>
@media (max-width: 768px) {
.heading {
white-space: pre-line;
}
}
ただし、改行コードをHTMLに直接埋め込む必要があり、多言語対応や動的なテキストへの適用がやや複雑になります。
display: blockな<span>をやめる
display: blockなspanが問題を引き起こすため、代わりにdisplay: inlineのまま
、テキスト幅のコントロールで見た目上の改行を作る方法もあります。ただし、テキス
ト量やフォントサイズによっては意図した位置で折り返せないことがあります。
いずれのCSSアプローチも、VoiceOverでのノード分割が起きるケースが、残る可能性があります
今回調べた中だと、aria-labelとの併用が一番手軽でHTMLの構造もこうすことなく対応できそうだと感じました。
まとめ
-
h1〜h6の中に<br>やdisplay: blockな<span>を置くと、SafariのAX実装でテキストノードが分割され、VoiceOverが「見出し レベルN」を繰り返し読み上げる -
最小変更で直すなら、見出し要素に
aria-labelを追加して、VoiceOverに読ませるテキストを上書きする -
根本から直すなら、
<br>を使わずCSSで折り返しを制御する。ただしdisplay: blockなspanも同様の問題が起きるため、aria-labelとの併用が安心
