aria-labelとaria-labelledbyを併用する場合とは
アクセシビリティを考慮したマークアップをした経験がある方は、aria-label
やaria-labelledby
についてご存じでしょう。これらは、要素にラベル付けするためのWAI-ARIAプロパティです。多くの場合、要素のアクセシブルな名前 (accessible name) を決めるために使われます。
aria-label
はラベルを直接文字列として指定するプロパティで、aria-labelledby
はIDを通じて他の要素をラベルとして指定するプロパティです。その使い分けについては、仕様書[1]で以下のように説明されています。つまり、可能な場合はaria-labelledby
に寄せるべきだということです。
If the label text is available in the DOM (i.e. typically visible text content), authors SHOULD use aria-labelledby and SHOULD NOT use aria-label.
(訳)もしラベルテキストがDOM内に(典型的には可視テキストコンテンツとして)存在する場合、著者は
aria-labelledby
を使うべき (SHOULD) であり、aria-label
を使うべきではありません (SHOULD NOT)。
aria-labelとaria-labelledbyの併用
ところで、aria-label
とaria-labelledby
を併用することはできるのでしょうか。適当に検索するとそんなことすべきでないというような回答が得られますが、ここで知りたいのはすべきかどうかではなく、できるのかどうかです。
MDNを紐解くと次のような記載があります。
Don't use both on the same element because
aria-labelledby
will take precedence overaria-label
if both are applied.(訳)両方を同じ要素に使わないでください。両方が使用された場合、
aria-labelledby
がaria-label
を上書きしてしまうからです。
つまり、両方を指定した場合aria-labelledby
が優先されるため、両方を指定する意味がないということです。
実際に試してみましょう。次のようなHTMLをGoogle Chromeで調べてみます。最近のブラウザの開発者ツールは、アクセシビリティ関連の情報も表示してくれます。この場合、input要素のアクセシブルな名前がどうなっているか調べればいいはずです。
<div id="label1">ラベルです</div>
<p>
<input type="text" aria-label="aria-labelです" aria-labelledby="label1" />
</p>
Google Chromeの開発者ツールで調べると、次の画像のような情報が表示されます。
この画像から分かるように、input要素のアクセシブルな名前は確かにaria-labelledby
に従っています。しかも、aria-labelには打消し線が引いてあり、その値が採用されていないことが示されています。
MDNに書かれているように、aria-label
とaria-labelledby
を同じ要素に対してした場合はaria-labelledby
が採用され、aria-label
の値が使われないことが確かめられました。
React Ariaさん!?
しかし、この記事はここでは終わりません。筆者がこの記事を書こうと思ったのは、業務でReact Ariaを使っていた際にあることに気付いたからです。
例えば、React Aria ComponentsのDatePickerコンポーネントを使った場合、レンダリング結果には次のようなHTML断片が含まれます。
<div
role="spinbutton"
aria-valuenow="2024"
aria-valuetext="2024"
aria-valuemin="1"
aria-valuemax="9999"
id="react-aria6233036342-:rs:"
aria-label="年, "
aria-labelledby="react-aria6233036342-:rs: react-aria6233036342-:rc:"
contenteditable="true"
spellcheck="false"
autocorrect="off"
enterkeyhint="next"
inputmode="numeric"
tabindex="0"
class="react-aria-DateSegment"
data-rac=""
data-type="year"
aria-describedby="react-aria-description-0"
>
2024
</div>
真ん中あたりを見ると、aria-label
とaria-labelledby
が両方指定されていることが分かります。何でこんなことをしているのか? というのがこの記事の本題です。
React Ariaのアクセシビリティ関連の実装は非常に優れていることが知られていますから、ミスではないはずです。しかし、先ほど調べたように、この場合はaria-labelledby
しか使われないはずです。では、aria-label
は何のために指定されているのでしょうか。
ここでは、arial-labelledby
にはスペース区切りで2つのIDが指定されています。このように複数の要素を指定した場合、ラベルとしては各要素のテキストが連結されたものになります。
そして、1つ目のIDをよく見てください。すると、1つ目のIDは自分自身を指定していることが分かります。
結論から言ってしまえば、aria-labelledbyで指定するIDとして自分自身を指定した場合、自分自身が表すラベルとしてaria-labelの値が使われるのです。aria-labelledby="自分自身のID 他の要素のID"
とした場合、自分自身のaria-label
と、他の要素のラベルを結合したものがアクセシブルな名前になります。
つまり、aria-label
とaria-labelledby
がこのように併用されているのは、他の要素由来のラベルとただの文字列のラベルを結合して使うためのハックであると言えます。
上の例を一部再掲します。
<div
<!-- ... -->
id="react-aria6233036342-:rs:"
aria-label="年, "
aria-labelledby="react-aria6233036342-:rs: react-aria6233036342-:rc:"
<!-- ... -->
>
aria-labelledby
で指定された1つ目のIDは自分自身です。2つ目のIDが示す要素は実は以下のものであり、ラベルとしてはDate
であることが分かります。よって、このdiv要素のアクセシブルな名前は年, Date
となります。
<span class="react-aria-Label" id="react-aria6233036342-:rc:">Date</span>
仕様書で確かめる
以上では、React Ariaで使われているテクニック(というかハック)を紐解くことで、aria-labelledby
で自分自身のIDを指定した場合は、本来無視されるはずのaria-label
の値を拾うことができることが分かりました。たしかに、単純に考えると無限ループになってしまうので何らかの特殊な挙動は必要です。
ということで、この挙動がちゃんと仕様に沿ったものであることを確かめましょう。今回参照するのは、Accessible Name and Description Computation 1.2
W3C Working Draft 02 August 2024です。
この仕様には、aria-label
等からアクセシブルな名前の値を計算するアルゴリズムが示されています。具体的には、4.3.2 Computation stepsです。
特に、ノードのaria-labelledby
プロパティに基づいて名前を計算するところを引用します。
LabelledBy: Otherwise, if the current node has an aria-labelledby attribute that contains at least one valid IDREF, and the current node is not already part of an ongoing aria-labelledby or aria-describedby traversal, process its IDREFs in the order they occur:
- Set the accumulated text to the empty string.
- For each IDREF:
- Set the current node to the node referenced by the IDREF.
- LabelledBy Recursion: Compute the text alternative of the current node beginning with the overall Computation step. Set the result to that text alternative.
- Append a space character and the result to the accumulated text.
- Return the accumulated text if it is not the empty string ("").
(訳)LabelledBy: さもなければ、もしcurrent nodeが少なくとも1つの有効なIDREFを含むaria-labelledby属性を持ち、かつcurrent nodeがすでに進行中のaria-labelledbyまたはaria-describedbyのトラバーサルの一部でない場合、そのIDREFを指定された順序で処理する。
- accumulated textを空の文字列に設定する。
- 各IDREFについて:
- IDREFで参照されるノードをcurrent nodeに設定する。
- LabelledBy Recursion: current nodeの代替テキストを計算する(Computationステップから行う)。resultを計算された代替テキストとする。
- accumulated textにスペース文字とresultを追加する。
- accumulated textが空文字列でなければ、それを返す。
特に2-2の部分で、aria-labelledby
で指定された要素に対してラベルを計算する再帰的な処理があります(Computationステップというものが言及されていますが、これは計算処理全体を指すものです)。
ポイントは、「current nodeがすでに進行中のaria-labelledbyまたはaria-describedbyのトラバーサルの一部でない場合」という条件です。この条件により、2-2で自分自身へ再帰したときの無限ループを回避しています。この場合、*LabelledBy:*ステップの処理に入りませんから、自分自身に再帰したときはaria-labelledby
が使われないことになります。
詳細は省きますが、アルゴリズムには上のLabelledByという分岐の次に、input要素(など)の入力値を使用するEmbedded Controlステップ、そしてその次にaria-label
を参照するAriaLabel分岐が続きます。よって、再帰してきてLabelledBy分岐に入れなかった場合、そのあとのAriaLabel分岐に入ってaria-label
の値が使われることになります。
以上のことから、aria-labelledby
で自分自身のIDを指定した場合には自身のaria-label
の値が使われるという挙動が仕様に沿ったものであることが分かりました。
余談ですが、このアルゴリズムでaria-labelledby
の処理がaria-label
よりも先に来ていることにより、aria-label
よりもaria-labelledby
が優先されるということが示されています。よく見ると3で「accumulated textが空文字列でなければ、それを返す」とありますから、aria-labelledby
のルールに従って計算しても空文字列しか得られなかった場合は、aria-label
のほうが使われることになります。
フォールバックとしてはありかもしれませんが、普通はaria-labelledby
でわざわざIDを指定する以上はそのIDが指す要素に適切なラベルがあるでしょうから、この挙動を活用することはあまり無さそうです。
まとめ
この記事では、aria-label
とaria-labelledby
を併用するという実装をReact Ariaが行っていたことをきっかけに、その場合の挙動について調べて解説しました。
結論としては、aria-labelledby
で自分自身のIDを指定した場合には自分自身のaria-label
の値が使われるため、このケースにおいては併用する意味があることが分かりました。これは、仕様に沿った挙動であり、他の要素由来のラベルとただの文字列のラベルを結合して使うためのハックとして使われています。
-
[https://www.w3.org/TR/wai-aria-1.3/](Accessible Rich Internet Applications (WAI-ARIA) 1.3
W3C First Public Working Draft 23 January 2024) ↩︎
Discussion