👨‍🎓

WAI-ARIAを学ぶときに整理しておきたいこと

10 min read

結論

  1. ロールについて知る
  2. HTMLの暗黙のロールを知る
  3. ロールを知った上でロールに対して使用できるプロパティ/ステートを使う
  4. (おまけ) markuplintを使おう

aria属性を使う前に

まず、いきなりaria-labelaria-selectedとかに手を出さない。

aria-selectedとかを発見してしまうと「option要素以外にもselectedみたいな意味を付加できるんだ!すげえ!使ってみよう!」みたいな気持ちが沸き上がってしまう。わかる。とってもよくわかるよ。当時ぼくもそうだったから。

ただ、そこはぐっと我慢してほしい。 なぜかと言うと、aria属性は、使っていいときと悪いときがある。きちんとWAI-ARIAという仕様と、ARIA in HTMLCore Accessibility API Mapping (Core-AAM)という仕様で決められていっている[1]ので、それに準拠することがアクセシブルなコンポーネント作るうえで重要だ。ARIAは、悪い使い方や組み合わせでは目的と逆にアクセシビリティを低くしてしまう。気をつけないといけない。まずは落ち着いて、仕様を理解しよう。

とは言っても仕様を全部完全に理解するのは難しい…。そこで、今回は、どういう順番で確認していくと、勘違いせずに使えるようになるのか、ぼくの経験から紹介したい。

以下の内容は、勧告候補段階のWAI-ARIA1.2を中心に書いているため、確定した情報でない場合があります。Chromeなどのブラウザが現状、WAI-ARIA1.1ではなく1.2相当の仕様(genericロールなどが存在)で動作しているため、それに合わせて意図的に1.2を中心にしています。ARIA in HTML、Core-AAM、Accnameに関しても同様です。

ロールがあって、そしてプロパティとステートがある

aria属性は「プロパティ」もしくは「ステート」と呼ばれるものを設定する属性だ。たとえばaria-labelはプロパティで、aria-selectedはステートだ。雑に分けると、静的な情報がプロパティで、動的な情報がステートになる。

そしてここからが重要で、そのプロパティとステートは、ロールに対してのプロパティとステートということだ。

たとえば、

<button aria-label="ヘルプ">?</button>

コードとしてはbutton要素にaria-label属性を設定しているが、「button要素のaria-labelという属性」を設定しているというよりも、「buttonロールのaria-labelプロパティ」を設定しているということを意識する必要がある。

ロール?突然だな?と思うかもしれないが、ロールが超重要。なぜかというとプロパティ/ステートは、ロールに基づいて設定できるもの・できないものが決まっているbuttonというロールにはaria-labelが指定できるので、例のコードが成り立つのだ。

だから、プロパティやステートを設定する前に、ロールというものを知る必要がある。

ロールについて

じゃあロールとは何なのか。ロールを理解するには、次について知っておくとよい。

  • 暗黙のロール
  • role属性
  • 抽象ロール

暗黙のロール

WAI-ARIAは、HTML5よりも歴史が古く、もともとHTML4の時代から存在していた。その名の通りアクセシブルなリッチインターネットアプリケーション(つまり今でいうWebアプリ)を作るための仕様だったので、その当時のHTMLにない語彙が規定された[2]。そしてHTML5になったときにはWAI-ARIAの一部がHTMLに輸入されて、要素や属性が大量に増えたという経緯がある。

そして、HTMLの各要素には暗黙的にロールが規定されることなった。button要素ならbuttonロール、a要素ならlinkロールといった具合に、すべての要素に何かしらのWAI-ARIAで規定されたロールが対応する。これを暗黙のロール[3]という。

HTML要素 対応する暗黙のロール
a link
button button
img image
header banner[4]
footer contentinfo[5]
nav navigation
ul list
li listitem
div generic
span generic

さてここで思い出して欲しいのは、プロパティ/ステートはロールによって設定できる・できないが決まっているということ。ということはつまり、aria属性は要素によって設定できる・できないが決まっているということだ。最初に述べたことにもどるけれど、こういう規定があるのでaria属性の設定は闇雲にしてはいけない。

role属性

未だにHTMLに輸入されてない語彙もWAI-ARIAには存在する。たとえばタブUIを構成するための tab tablist tabpanel がそうだ。HTMLに <tab> <tablist> <tabpanel> ができてくれると嬉しいけど、現段階では存在しない。そんなときに要素にロールを与えるのがrole属性だ。

<ul role="tablist">
	<li role="tab">tab1</li>
	<li role="tab">tab2</li>
	<li role="tab">tab3</li>
</ul>
<div role="tabpanel">content1</div>
<div role="tabpanel">content2</div>
<div role="tabpanel">content3</div>

このようにrole属性を使えば、要素に任意のロールを与えることができる。

しかし、このrole属性を使用する場合には次のことに注意しよう。

上書きできないロールがある

<!-- 👎 ダメ -->
<img role="banner" />

<!-- 👍 許される(けど後述に注意) -->
<img role="button" />

各要素に上書きできるロールは決まっていて、 img要素の場合、buttonlinkなどのロールは与えることができるがbannerは与えることができない[6]

role属性自体も無闇に指定できるものではない、と覚えておこう。

既に要素に対応するものがあるロールは使用するべきではない、既存の要素をつかう

そして、img要素にbuttonロールを与えられるとしても、buttonロールは既にbutton要素が暗黙のロールをもっているので、そちらを利用するべきだ[7]

<!-- 👎 良くない -->
<img role="button" />

<!-- 👍 こっちが良い -->
<button><img /></button>

特にbutton要素とbuttonロールは注意が必要で、role属性でロールの上書きが成功したとしてもそれはアクセシビリティツリーのロールが変わっただけで、振る舞いがすべてbutton要素と同じになったわけではない。tabindex属性を使わなければフォーカス可能にできないし、clickイベントハンドラを設定しても、button要素ならEnterキーでも発火するが、その他の要素は発火しない。

暗黙のロールを明示しない

暗黙的にもっているロールを敢えてrole属性で指定することは冗長なので避ける[8]

<!-- 👎 良くない -->
<nav role="navigation">...</nav>

<!-- 👍 こっちが良い -->
<nav>...</nav>

抽象ロール

ロールには、スーパークラスやサブクラスといったモデル体系のヒエラルキーが存在する。スーパークラスの一部は抽象ロールとされていて、使用できない。たとえばinputというロールが存在するが、このロールはcheckboxロールやradioロールの元となるスーパークラスロールで、role属性などで使うことはできない。

とりあえず使用できないロールもあると覚えておいて、仕様を読むときに抽象ロールかどうか確認できればOKだ。

ロールに規定されること

ロールには、それに基づいて設定できるプロパティ/ステートが決まっているように、他にも知っておくべき規定がある。

  • アクセシブルな名前が必須かどうか
  • アクセシブルな名前がコンテンツから得られるかどうか[9]
  • アクセシブルな名前の明示指定が禁止かどうか[10]
  • 必須プロパティ/ステート
  • 禁止プロパティ/ステート
  • 非推奨(廃止予定)プロパティ/ステート
  • プロパティ/ステートのデフォルト値
  • 必須の構造
  • 子のプレゼンテーション[11]

アクセシブルな名前

アクセシブルな名前とは、そのロールが持つ名前で、コンテンツ(要素が内包する内容やテキスト)やaria-labelで設定されたもので、たとえば次のようになる。

<!-- 「押すなよ、絶対押すなよ」というアクセシブルな名前を持つ -->
<button>押すなよ、絶対押すなよ</button>

<!-- 「ヘルプ」というアクセシブルな名前を持つ -->
<button aria-label="ヘルプ">?</button>

aria-labelで設定できるかどうか、コンテンツから設定できるかどうかはロールによって異なる。先の例のbuttonロールでは「コンテンツ由来[12]」であり「著者由来[13]」であるので、どちらでも設定可能だ。

「コンテンツ由来」でない、つまりコンテンツはアクセシブルな名前にならないロールもある。

<!-- アクセシブルな名前は持たない -->
<nav>
	...ナビゲーション...
</nav>

<!-- 「メインメニュー」というアクセシブルな名前を持つ -->
<nav aria-label="メインメニュー">
	...ナビゲーション...
</nav>

<!-- 「メインメニュー」というアクセシブルな名前を持つ -->
<nav aria-labelledby="nav-name">
	<h2 id="nav-name">メインメニュー</h2>
	...ナビゲーション...
</nav>

navigationロールは「著者由来」だけだからだ。

そして重要なのが、アクセシブルな名前を持てないロールもある。持つことを禁止されている[14]ので、同時にaria-labelaria-labelledbyが設定禁止になっている。

<!-- 👎 genericロールはアクセシブルな名前を持てないのでNG -->
<div aria-label="無効なラベル">...</div>

<!-- 👎 strongロールはアクセシブルな名前を持てないのでNG -->
<strong aria-label="無効なラベル">...</strong>

ちなみに、「コンテンツ由来」だけのロールは存在しない。

コンテンツやaria-labelからアクセシブルな名前が決定される優先順位はAccessible Name and Description Computation(通称「Accname」)によって計算される。実際にはHTMLのその他の属性やCSSの表示状態よっても決定されるので、気になる場合は仕様を読んでみるといい。

プロパティ/ステートの必須・禁止・非推奨

それぞれしっかりロールごとに決まっているので、仕様を確認すると良い。

プロパティ/ステートのデフォルト値

これもロールごとにそれぞれデフォルト値の決まっているプロパティ・ステートがある。

必須の構造

li要素がul要素の子要素としか存在できなように、ロールもまた構造ルールをもつものがある。listロールとlistitemロール、menuロールとmenuitemロール、tablistロールとtabロールなど。組み合わせもさまざまなので、注意深く確認したほうがいいだろう。

その中でpresentationロールは特殊な定義となっている。

<ul role="tree">
	<li role="presentation">
		<a role="treeitem">ツリーアイテム</a>
	</li>
</ul>

treeロールとtreeitemロールの構造は互いに必須だが、presentationロールを挟むことができる[15]

プロパティとステート

やあ、お待たせ。ここまでくれば、やっとプロパティとステートの説明ができる。ロールが理解できたのなら、あとはその目的のロールに合わせてプロパティを使っていくだけだ。

論理属性ではない

プロパティ/ステートを設定するときの注意点として、aria属性は論理属性ではないということだ。論理値をとるステートだとしても、文字列として設定する必要がある。

<!-- 👎 無効 -->
<button aria-pressed>ボタン</button>

<!-- 👍 有効 -->
<button aria-pressed="true">ボタン</button>
<button aria-pressed="false">ボタン</button>

また、未設定の場合は未設定の値undefinedとなるプロパティ/ステートもあるので、よく仕様を確認すること。ちなみにaria-pressedは、true/false/mixed/undefinedを受け付ける。

HTMLとは異なる文化で定義された仕様ということを意識しておくと、よいかもしれない。

暗黙のプロパティ/ステート

ここまでの関係性がわかってくると、次のコードの問題点を予想できるんじゃないだろうか。

<input type="checkbox" aria-checked="false" />

このチェックボックスは、ユーザーがチェックをしてもARIAのステートは false のままだ。ブラウザに描画される見た目や、DOMプロパティ[16]としては「チェックされている」状態にきちんとなっていても、ARIAステートは「チェックされていない」ことになる。スクリーンリーダーほか支援技術だけクリティカルな問題となる一番避けたい重大なバグを作ってしまう。JavaScriptを使ってchangeイベントなどで適切にステートを切り替えていれば問題にはならないが、だったらそもそもaria-checkedを指定しないほうがいい。わざわざスクリプトで制御する必要もなくなる。

仕様でもこれは言及されていて[17]、次のステートは特に注意するとよい。

ステート 注意すべきロール
aria-checked checkbox radio
aria-haspopup combobox
aria-selected option
aria-multiselectable combobox listbox
aria-valuemax slider meter progressbar
aria-valuemin slider meter

意味の重複定義

また、意味の同じプロパティ/ステートと属性を同時に設定することをARIA in HTMLでは「すべきでない」とされている[18]暗黙のロールを明示しないと同じ扱いだと思えばよい[19]。殆どの場合、HTMLの本来持っている属性の振る舞いをaria属性は保証しないので、独自に実装しないといけない。そんなことならaria属性はやっぱり指定するべきではない。もちろん、矛盾する定義は禁止だ。

<!-- 👎 重複するべきではない -->
<input type="text" readonly aria-readonly="true" />

<!-- 👍 片方なら良い -->
<input type="text" readonly />

<!-- 🤔 禁止はされていないけど、結局readonly属性と同じ振る舞いを実装しないといけない -->
<input type="text" aria-readonly="false" />

<!-- 🚫 ダメ、ゼッタイ -->
<input type="text" readonly aria-readonly="false" />

まとめ

  1. aria属性が使いたくなったら…
  2. その要素の暗黙のロールを調べる。
  3. ロールが適切であればそのまま。他に最適なロールがあればそれを暗黙的にもつ要素に変える。要素がなければrole属性でロールを上書きする(上書きできない場合があるので注意)。
  4. ロールの必須プロパティ/ステートを設定する。
  5. 他に必要なプロパティ/ステートを設定する。
  6. 必要な構造関係があれば、暗黙ロールをもつ要素や任意にロールをもたせた要素を追加する。
  7. プロパティ/ステートの値が正しいか確認する。

(おまけ) markuplintを利用する

最後に、著作が開発しているmarkuplintを紹介させてほしい。

markuplintはWAI-ARIAをチェックするルール(wai-aria 日本語版)。これまで紹介した規格のほぼ全てを機械的に見つけることができる。特にVisual Studio Codeの拡張をインストールすれば、コーディング中に問題を検知できる。何が間違いなのかフィードバックを受けながら開発ができるので、学習面でも利用価値があると思っている。HTMLに限らずPugやEJS、そしてReactやVueやSvelteにも対応しているので、是非使ってみて欲しい。

脚注
  1. 正式勧告されたわけではないので継続的に新しい機能が追加されていっている段階 ↩︎

  2. HTMLに限らず、SVGやXMLなど、他のマークアップ言語にも適応できる前提で今も昔も規定されています。 ↩︎

  3. 「暗黙のセマンティクス」という言い方をされることもある。 ↩︎

  4. 条件に拠る: https://www.w3.org/TR/html-aria/#el-header ↩︎

  5. 条件に拠る: https://www.w3.org/TR/html-aria/#el-footer ↩︎

  6. これはWAI-ARIAではなく、ARIA in HTMLで規定される。 https://www.w3.org/TR/html-aria/#el-img ↩︎

  7. じゃあ、なんで上書きできるようになってるんだろうね。禁止しちゃえばいいのにね。 ↩︎

  8. スクリーンリーダなどの支援技術で、古いものはHTMLとARIAのマッピングができていないものもあるので、それに対応するために致し方なく指定する場合はある。レガシーなブラウザへのサポートと同様に方針を立てて対応すべし。 ↩︎

  9. 得られる場合、aria-labelaria-labelledbyが無くとも要素の内容(もしくはalt属性やtitle属性)からアクセシブルな内容が決定する。 ↩︎

  10. 禁止される場合、aria-labelaria-labelledbyの設定が禁止される。アクセシブルな名前はもたない。 ↩︎

  11. 著者もまだ理解が及んでいない。詳しい方、フォロープリーズ ↩︎

  12. 仕様では"Name From: content"と定義される。innerTextが名前となる。 ↩︎

  13. 仕様では"Name From: author"と定義される。「著者」という少々わかりにくい表現であるが、aria-labelもしくはaria-labelledbyで明示的に設定されたものが名前となる。 ↩︎

  14. 仕様では「Name From: prohibited」と定義される。 ↩︎

  15. このトランスペアレントに振る舞えることに言及した部分を仕様に見つけることができなかった。https://www.w3.org/TR/wai-aria-1.2/#example-12 ではセマンティックスの上書きに言及されているけど…。 ↩︎

  16. checkedプロパティで参照できる。 https://developer.mozilla.org/ja/docs/Web/API/HTMLInputElement#Properties_checkbox_radio ↩︎

  17. ARIA in HTMLで"Authors SHOULD NOT use the aria-checked attribute on input type=checkbox elements."と言及されている。 https://www.w3.org/TR/html-aria/#el-input-checkbox ↩︎

  18. Document conformance requirements for use of ARIA attributes with HTML attributes ↩︎

  19. ロールと同様、非対応な支援技術のために仕方なく設定する場合もあるだろう。その場合、属性値が変化するならばプロパティ/ステートも同期的に変化するように実装しなければならない。 ↩︎

この記事に贈られたバッジ

Discussion

ログインするとコメントできます