ARIA

ARIA(Accessible Rich Internet Applications)は、HTML だけでは表現できない意味付けを補うための属性群。 強力だが、誤用すると「a11y を悪化させる」道具にもなる。

5 つのルール(W3C)

1. ARIA を使わなくて済むなら使わない

ネイティブの HTML 要素で同じ意味が表現できるなら、それを使う。
「div + ARIA」より「適切な HTML 要素」のほうが常に良い

<!-- BAD -->
<div role="button" tabindex="0" onclick="...">送信</div>

<!-- GOOD -->
<button type="button" onclick="...">送信</button>

2. ネイティブのセマンティクスを変えない

<h1 role="button"> のように、本来の意味を上書きしない。スクリーンリーダの利用者が混乱する。

3. すべてのインタラクティブ ARIA 要素はキーボードで操作可能

role="button" を付けたら、Tab で来る・Enter / Space で発火するように JS で実装する必要がある。
<button> ならこれが全部タダ。

4. role="presentation" / aria-hidden="true" をフォーカス可能要素に付けない

フォーカスは入るのにスクリーンリーダから見えない、という最悪の状態になる。

5. インタラクティブ要素はアクセシブルな名前を持たなければならない

ボタン・リンク・入力欄には必ず「何のもの」かの名前が必要。
<label>aria-labelaria-labelledby、内部テキストのいずれかで。

3つのカテゴリ

1. role — 種別

要素が「何の種類のもの」かを表す。HTML タグの暗黙の role を上書き、または div/span に追加する。

<div role="alert">エラーが発生しました</div>
<ul role="tablist"> ... </ul>

2. aria-*(プロパティ) — 静的な属性

要素についての「変わらない情報」。

3. aria-*(ステート) — 動的な状態

要素の「現在の状態」。JS で値を更新する。

名前の付け方(accessible name)

要素の「名前」は以下の優先順位で決まる:

  1. aria-labelledby
  2. aria-label
  3. 関連付けされた <label>(input の場合)
  4. 要素の内部テキスト(button の場合)
  5. title 属性(最後の手段)

典型例

<!-- アイコンのみのボタン -->
<button aria-label="削除">
  <svg aria-hidden="true">...</svg>
</button>

<!-- 入力欄 -->
<label for="name">氏名</label>
<input id="name" />

<!-- 検索フォーム(label を出したくない場合) -->
<input type="search" aria-label="検索" />

<!-- 別要素を参照 -->
<h2 id="dialog-title">確認</h2>
<div role="dialog" aria-labelledby="dialog-title"> ... </div>
aria-label の使いすぎに注意

aria-label視覚に出ないテキスト。視覚的なラベルが既にあるならそちらを使う方がいい (音声入力で「○○ボタンを押して」と言える等の利点がある)。
視覚ラベルと aria-label が違うと、音声入力ユーザーが「見えるラベルで言ったのに反応しない」事態になる。

よく使う role と用途

ランドマーク

基本は HTML 5 タグで自動付与される。<main> = main、<nav> = navigation 等。 div しか使えない歴史的な事情があるなら role で代替。

ライブリージョン

role用途
role="alert"緊急の通知(赤いエラー等)。即座に読み上げ。
role="status"状態通知("保存しました" 等)。控えめに読み上げ。
role="log"追加され続けるログ(チャットメッセージ)。
role="timer"タイマー表示。

ダイアログ系

タブ・メニュー

選択系

典型的な実装パターン

展開ボタン(アコーディオン・dropdown)

<button
  type="button"
  aria-expanded="false"
  aria-controls="panel-1"
>
  詳細を表示
</button>
<div id="panel-1" hidden>
  ... 中身 ...
</div>

クリックで aria-expanded を切り替え、hidden 属性を付け外し。

トグルボタン

<button type="button" aria-pressed="false">
  サイレント
</button>

押されたら aria-pressed="true" に。SR は「押されています、サイレント、ボタン」と読み上げる。

現在地(パンくず・ナビ)

<nav aria-label="グローバル">
  <ul>
    <li><a href="/">ホーム</a></li>
    <li><a href="/a11y/" aria-current="page">a11y</a></li>
  </ul>
</nav>

モーダルダイアログ

<!-- ネイティブ  が一番楽。下は role で組む例 -->
<div
  role="dialog"
  aria-modal="true"
  aria-labelledby="confirm-title"
  aria-describedby="confirm-desc"
>
  <h2 id="confirm-title">確認</h2>
  <p id="confirm-desc">このアイテムを削除しますか?</p>
  <button type="button">キャンセル</button>
  <button type="button">削除</button>
</div>

エラーアラート

<div role="alert">
  メールアドレスの形式が正しくありません
</div>

動的な状況通知(ステータス)

<div role="status" aria-live="polite">
  3 件のメッセージを保存しました
</div>

aria-hidden の使い方

要素をスクリーンリーダから隠す。視覚的には見えるが SR から無視される。

フォーカス可能要素に aria-hidden は禁忌

aria-hidden="true" を付けた要素の中にボタンや input があると、Tab で来るのに SR では読まれない最悪の状態。実装中によくある事故。

「視覚的に隠すが SR には見せる」

スクリーンリーダ専用テキストを入れたい時の定番 CSS。display: nonevisibility: hidden は SR からも消えるのでダメ。

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
<button>
  <svg aria-hidden="true">...</svg>
  <span class="sr-only">削除</span>
</button>

避けるべきアンチパターン

判断フロー

  1. そのまま HTML タグで意味が出るか? → 出るなら何もしない
  2. 視覚ラベルがない/不十分か? → aria-label または aria-labelledby
  3. 状態を伝えたい? → aria-expanded / aria-pressed / aria-current
  4. 動的な情報を通知したい? → role="status" / role="alert" / aria-live
  5. カスタムウィジェットを作る? → APG をコピペ
参考: ARIA Authoring Practices Guide

W3C 公式のUI パターン集。タブ、メニュー、ツリービュー、ダイアログ、コンボボックス等の 実装方法(HTML + ARIA + キーボード操作)が体系的にまとまっている。
https://www.w3.org/WAI/ARIA/apg/
自前で凝った UI を作るときは、必ず該当パターンの実装例を確認する。