書籍『Every Layout』の著者の一人でもある、Andy Bell 氏の「A (more) Modern CSS Reset」という記事を読んでいて気になるコードがありました。

紹介されているリセット CSS の、リストに対する指定を以下に抜粋します。

ul[role='list'],
ol[role='list'] {
  list-style: none;
}

記事によると、Safari 上で VoiceOver を使用したときに、list-style: none が指定されていると、リストがリストとして認識されなくなるとのことです。

Safari と VoiceOver の組み合わせで、デフォルトのリスト要素を読み上げたときのスクリーンショット
デフォルトでは「リスト3項目」と読み上げられる
Safari と VoiceOver の組み合わせで、`list-style: none` を指定したリスト要素を読み上げたときのスクリーンショット
list-style: none を指定するとリストの情報が読み上げられない
Google Chrome と VoiceOver の組み合わせで、`list-style: none` を指定したリスト要素を読み上げたときのスクリーンショット
Google Chrome では list-style: none を指定しても「リスト3項目」と読み上げられる

このとき、<ul role="list"><ol role="list"> と、ARIA ロールを指定するとリストとして認識されるので、この問題を解消できます。さきほどのコードは、これらの指定がある場合のみ list-style: none が有効になるように意図されたもののようです。

筆者はこの現象については勉強不足で知りませんでした。すでに議論され尽くされているかもしれませんが、この記事では、なぜ Safari(WebKit)ではこのような実装になっているかの背景や意図を理解し、検証したうえで、本サイトでの方針を考えていきます。

この挙動がバグとして報告されたのは 2017 年の 3 月のようで、以下に Apple の James Craig 氏とのやりとりが記録されています。

コメント欄の最後(2023-01-09)に、過去の James Craig 氏 のツイートへのリンクがあり、そのスレッド内で詳しい背景や意図が語られています。

簡単にまとめると、VoiceOver を利用するユーザからのフィードバックをまとめたところ、ウェブ上にはリストが多すぎて何度もリストの情報が読み上げられるのがわずらわしく感じる、といった意見が寄せられたようです。

加えて、デザイナーが視覚的に不要だと判断してリストマーカーを無効にしているのにも関わらず、スクリーンリーダーのユーザにはリストのセマンティクスを残して、負担を強いる必要があるのかという趣旨の疑問を呈しています。

これらの点を踏まえて、デフォルトのリストの見た目を無効(list-style: none)にした場合には、リストとして認識しないように意図的に変更されたようです。

もし、list-style: none を設定した要素をリストとして認識させたい場合には、role="list" を加えることで明示的にロールを上書きできることも提示されています。


これらの一連のコメントやツイートや関連する記事を読んで、経緯や意図については理解できました。また、自身のマークアップを振り返り、そこまで深く考えずに <ul> を使用してきた点は反省しなければなりません。

その一方で、リスト要素のデフォルトの見え方であるリストマーカーを CSS で無効にしているからといって、視覚的にリストとして認識しないかと問われると、そこには疑問が残ります。

加えて、リストマーカーは CSS の制約が大きく、細かく位置やサイズを調整するのが難しいため、list-style を前提としたデザインや実装が避けられるのも事実です。

その対策として、リスト要素に role="list" を指定する方法が提示されているのですが、ネイティブな HTML 要素が持っている暗黙のロールを同一のロールで上書きするのはルールから逸脱するので、この方法には若干の抵抗があります。

このように、実装者の考えと相容れない部分もありますが、実際の VoiceOver ユーザの声を聞いたうえでの決定なので、そこには一定の説得力があるのかもしれません1

Safari と VoiceOver の組み合わせで、リストとして認識されるパターンを調べました。検証環境は Safari 16.3 ですが、環境によっては結果が異なるかもしれません。

試したパターンは以下のとおりです。

  • A. デフォルト
  • B. list-style: none を指定
  • C. <ul role="list"> を指定
  • D. <nav> の直下にリストを配置
  • E. <li>::before 擬似要素を指定
  • F. list-style: url() を指定
  • G. list-style: '' を指定

ライブデモは以下よりご確認いただけます。

何もスタイルを指定しない場合には、当然リストとして認識されます。

<ul>
  <li>foo</li>
  <li>bar</li>
  <li>baz</li>
</ul>

B. list-style: none を指定

見出し「B. 」

この <ul>list-style: none を指定すると、ここまでの説明のとおり、リストとして認識されなくなります。

<ul>
  <li>foo</li>
  <li>bar</li>
  <li>baz</li>
</ul>

<style>
ul {
  list-style: none;
}
</style>

Safari のデベロッパーツールで、<ul> のロールを確認すると「一致する ARIA ロールなし」と表示されます。ちなみに、子要素の <li> のロールは listitem のままです。

C. <ul role="list"> を指定

見出し「C. 」

list-style: none を指定した <ul>role="list" を指定すると、再びリストとして認識されます。

<ul role="list">
  <li>foo</li>
  <li>bar</li>
  <li>baz</li>
</ul>

<style>
ul {
  list-style: none;
}
</style>

D. <nav> の配下にリストを配置

見出し「D. 」

<ul>list-style: none が指定されていても、<nav> 配下に置かれている場合にはリストとして認識されます。

<nav>
  <ul>
    <li>foo</li>
    <li>bar</li>
    <li>baz</li>
  </ul>
</nav>

<style>
ul {
  list-style: none;
}
</style>

なお、実際のケースではあり得ませんが、親要素が <div role="navigation"> でも同様の結果になり、<nav role="presentation"> であればリストとして認識されなくなります。

また、<nav><ul> の間に別の要素が存在していてもリストとして認識されます。

つまり、リストに list-style: none が指定されていても、navigation ロールを持つ祖先要素が存在する場合にはリストとして認識されます。

E. <li>::before 擬似要素を指定

見出し「E. 」

<ul>list-style: none が指定されている状態で、<li>::before 擬似要素を指定します。この擬似要素はレイアウトに影響を及ぼさないようにしたいので、ゼロ幅スペースである \200B を指定します2

このテクニックは MDN で紹介されている方法ですが、リストとして認識されました。

<ul>
  <li>foo</li>
  <li>bar</li>
  <li>baz</li>
</ul>

<style>
ul {
  list-style: none;
}
ul li::before {
  content: '\200B';
}
</style>

以下のように、<ol>list-style: none を指定し、<li>::before 擬似要素でカウンターを指定する場合も同様にリストとして認識されました。

<ol>
  <li>one</li>
  <li>two</li>
  <li>three</li>
</ol>

<style>
ol {
  list-style: none;
}
ol li {
  counter-increment: count;
}
ol li::before {
  content: counter(count)'. ';
}
</style>

なお、content: '' のように空文字を指定した場合にはリストとして認識されません。

F. list-style: url() を指定

見出し「F. 」

こちらも同様に MDN で紹介されている方法です。list-style に dataURL で空の SVG を指定するテクニックで、同様にリストとして認識されました。

<ul>
  <li>foo</li>
  <li>bar</li>
  <li>baz</li>
</ul>

<style>
ul {
  list-style: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg"/%3E');
}
</style>

G. list-style: '' を指定

見出し「G. 」

list-style: '' で空文字を指定するパターンです。MDN のページでは空文字では有効にならないと説明されているのですが、リストとして認識されました。

<ul>
  <li>foo</li>
  <li>bar</li>
  <li>baz</li>
</ul>

<style>
ul {
  list-style: '';
}
</style>

本サイトでは、リセット時にすべてのリスト要素に list-style-type: none を指定しています。そのため、Safari と VoiceOver の組み合わせでは、本来リストとして読み上げて欲しい要素であっても、リストとして認識されていないことを確認しました。

これらを修正する方法を検討しましたが、結果的にはあえて何も対応しないことにしました。

理由としては、role="list" でロールを上書きするのには抵抗がありますし、CSS ハックで対応する方法についても副作用への懸念が残るためです。

また、リストとして認識されなくても致命的ではなく、情報自体は取得できるので、釈然としない気持ちは残りますが、余計なことはせずにブラウザ側に解釈を任せることにしました。

ただ、これとは別に、マークアップ自体の妥当性については、見直していきたいと考えています。

本記事の作成にあたり、以下のウェブページを参考にしました。

脚注

  1. ツイートでは、HTML Design Principles の Priority of Constituencies が引用されています。

  2. ゼロ幅スペース | Wikipedia