HTML/CSS

WAI-ARIA、もう怖くない!「いつ・どこで・どう使うか」を徹底解説する実践ガイド

wai-area

そのARIA属性、自信を持って使っていますか?🤔💡

aria-label, role, aria-hidden…

なんとなく知っているけれど、「下手に使うと逆にアクセシビリティを損なう」という話を聞いて、WAI-ARIAに少し怖いイメージを持っていませんか?😟🔍

その不安、とてもよく分かります。ARIAは強力なツールですが、その役割を正しく理解しないと、良かれと思って追加した属性が、かえって混乱を招くこともあります。⚡🙈

この記事では、そんなARIAへの漠然とした不安を解消し、「いつ、どこで、どのように」使えば良いのかを、具体的な例と共に徹底的に解説します。もうARIAを恐れる必要はありません。強力な味方として使いこなしましょう📝🌈✨

WAI-ARIAとは? HTMLの「意味」を補う翻訳ツール

WAI-ARIAの役割は、一言で言えば「HTMLだけでは伝えきれない『意味』や『状態』を、スクリーンリーダーなどの支援技術に伝えるための翻訳ツール」です。🗣️🔄👂

HTML5では<nav><main>といったセマンティックなタグが増えましたが、JavaScriptで動的に作られる複雑なUI(例えば、タブ、スライダー、カスタムドロップダウンなど)の「役割」や「現在の状態」までは表現しきれません。🖥️🧩

そこでARIAの出番です。ARIAは、そうしたリッチなUIに対して

  • これは何ですか? → role(役割)
  • どんな機能を持っていますか? → aria-*プロパティ(特性)
  • 今どうなっていますか? → aria-*ステート(状態)

といった情報を付与し、支援技術がユーザーに正しく伝えられるよう手助けします。

見た目だけを整えるのではなく、コードに正しい「意味」を与えること。それが、ユーザー、検索エンジン、そして開発者自身にとっても大きな価値を生むのです。

【最重要ルール】ARIAを使う前に

第一に、ARIAを使うな⚠️

これは有名なルールで、「もしネイティブのHTML要素で同じ機能が実現できるなら、必ずそちらを優先する」という意味です。例えば、ボタンを作りたいなら<div>role="button"をつけるのではなく、必ず<button>要素を使いましょう。ネイティブ要素には、キーボード操作やフォーカス管理など、アクセシビリティに必要な機能が標準で備わっているからです

ARIAは、あくまでネイティブHTMLでは表現できない場合の「最後の切り札」なのです。

ARIA属性のトリセツ:「どこに」「何を」書くべきか?

ARIAの属性は、大きく分けて「ロール」「プロパティ」「ステート」の3種類です。それぞれの役割と使い方を見ていきましょう。

role属性:コンポーネントの「役割」を定義する

roleは、その要素がUIの中でどのような「役割」を持つかを定義します。主に、カスタムコンポーネントの親要素に指定します。

  • いつ使う?
    • <div><span>を使い、JavaScriptで独自のUI(ダイアログ、タブ、スライダー等)を実装したとき。
  • どこに書く?
    • コンポーネント全体を囲む親要素に書きます。
  • どう書く?
    • <div role="dialog">:これがダイアログであることを示す。
    • <ul role=”tablist”>:これがタブのリストであることを示す。
    • <div role=”alert”>:エラーメッセージなど、ユーザーに即時通知すべき内容であることを示す。

aria-*プロパティ:要素の「特性」を説明する

プロパティは、要素間の関係性や、あまり変化しない特性を説明します。

  • いつ使う?
    • 要素にテキストラベルがない場合や、他の要素との関係を明確にしたいとき。
  • どこに書く?
    • 説明が必要な要素そのものに書きます。
  • どう書く?
    • aria-label="テキスト": 最もよく使われる属性の一つ。要素に表示されているテキストがない場合(例:閉じるボタンのXアイコン)に、スクリーンリーダーに読み上げさせる「ラベル」を提供します。
      <div aria-label="閉じる">×</button>
    • aria-labelledby="ID": 既にページ内に表示されているテキストを、その要素のラベルとして紐付けます。
    <h2 id="dialog-title">設定</h2>
    <div role="dialog" aria-labelledby="dialog-title">

    </div>
    • aria-controls="ID":
      ある要素が、どの要素を制御しているかを示します。タブがどのタブパネルを操作するかに使われます。
      <button role=”tab” aria-controls=”panel-1″>タブ1</button>
      <div id=”panel-1″>…</div>

aria-*ステート:要素の「状態」を伝える

ステートは、ユーザーの操作によって変化する、要素の「現在の状態」を示します。JavaScriptによる状態の更新が必須です。

  • いつ使う?
    • アコーディオンの開閉、タブの選択状態など、UIの状態が動的に変わるとき。
  • どこに書く?
    • 状態が変化する要素そのものに書きます。
  • どう書く?
    • aria-expanded="true/false":
      要素が開いているか閉じているかを示します。アコーディオンやドロップダウンメニューのボタンに使います。
      <!– JSで ‘true’/’false’ を切り替える必要がある –>
      <button aria-expanded="false" aria-controls="menu">メニュー</button>
    • aria-selected="true/false": タブなどで、どの項目が選択されているかを示します。
    • aria-hidden="true/false": display: none; と似ていますが、スクリーンリーダーに対して要素を隠します。表示されているが、一時的に操作対象外にしたいモーダルダイアログの背景などに使います。
    • aria-checked="true/false/mixed":
      カスタムデザインのチェックボックスやラジオボタンが選択されているかを示します。

実践!タブUIをアクセシブルに改善する

HTMLの良い書き方・悪い書き方

<div class="button" onclick="doSomething()">クリックしてください</div>
    

❌ キーボード操作できず、スクリーンリーダーもボタンと認識できません。

<div>
  <h2>HTMLの良い書き方・悪い書き方</h2>
  
  <!-- タブボタン -->
  <div role="tablist" aria-label="コードの悪い例と良い例">
    <button role="tab" id="tabBad" class="tab-btn active" aria-selected="true" aria-controls="badExample">悪い例</button>
    <button role="tab" id="tabGood" class="tab-btn" aria-selected="false" aria-controls="goodExample" tabindex="-1">良い例</button>
  </div>
  
  <!-- 悪い例 -->
  <div role="tabpanel" id="badExample" aria-labelledby="tabBad">
  ・・・
  </div>

  <!-- 良い例 -->
  <div role="tabpanel" id="goodExample" aria-labelledby="tabGood" style="display:none;">
  ・・・
  </div>
</div>

<script>
    const tabBad = document.getElementById('tabBad');
    const tabGood = document.getElementById('tabGood');
    const badExample = document.getElementById('badExample');
    const goodExample = document.getElementById('goodExample');

    function switchTab(selectedTab, unselectedTab, selectedPanel, unselectedPanel) {
        // クラスの切り替え
        selectedTab.classList.add('active');
        unselectedTab.classList.remove('active');

        selectedTab.setAttribute('aria-selected', 'true');
        unselectedTab.setAttribute('aria-selected', 'false');

        // tabindexの管理(キーボード操作性向上のため)
        selectedTab.removeAttribute('tabindex');
        unselectedTab.setAttribute('tabindex', '-1');

        // パネルの表示/非表示
        selectedPanel.style.display = 'block';
        unselectedPanel.style.display = 'none';
    }

    tabBad.addEventListener('click', () => {
        switchTab(tabBad, tabGood, badExample, goodExample);
    });

    tabGood.addEventListener('click', () => {
        switchTab(tabGood, tabBad, goodExample, badExample);
    });
</script>

では、今学んだ知識を使って、よくあるタブUIを改善してみましょう。以下のコードは、まだWAI-ARIAが適用されていない状態です。

悪い例:WAI-ARIAが適用されていないタブUI

<div>
    <!-- タブボタン -->
    <div>
        <button id="tabBad" class="tab-btn active">悪い例</button>
        <button id="tabGood" class="tab-btn">良い例</button>
    </div>

    <!-- 「悪い例」のコンテンツ -->
    <div id="badExample">
   ・・・
    </div>

    <!-- 「良い例」のコンテンツ (初期状態では非表示) -->
    <div id="goodExample" style="display:none;">
   ・・・
    </div>
</div>

<script>
    const tabBad = document.getElementById('tabBad');
    const tabGood = document.getElementById('tabGood');
    const badExample = document.getElementById('badExample');
    const goodExample = document.getElementById('goodExample');

    tabBad.addEventListener('click', () => {
        tabBad.classList.add('active');
        tabGood.classList.remove('active');
        badExample.style.display = 'block';
        goodExample.style.display = 'none';
    });
    tabGood.addEventListener('click', () => {

        tabGood.classList.add('active');
        tabBad.classList.remove('active');
        goodExample.style.display = 'block';
        badExample.style.display = 'none';
    });
</script>

このコードは、一見すると問題なく動作しているように見えます。しかし、スクリーンリーダーなどの支援技術を使っているユーザーにとっては、以下のような大きな問題を抱えています。
アクセシビリティ上の課題が3つあります🚨♿

  1. 役割が不明 (Role is unclear):
    スクリーンリーダーは、これが「タブ機能」だと認識できません。単に「悪い例 ボタン」「良い例
    ボタン」と、2つのボタンが並んでいるとしか解釈しません。
  2. 状態が不明 (State is unclear):
    「悪い例」タブが選択されている状態(アクティブであること)が、CSSのactiveクラスによる背景色でしか表現
    されていません。スクリーンリーダーのユーザーには、どちらのタブが選択されているのか伝わりません。
  3. 関連性が不明 (Relationship is unclear):
    「悪い例」ボタンを押すと「悪い例」のコンテンツが表示される、という2つの要素の関連性が、プログラム的に
    紐付いていません。視覚的に判断するしかないため、スクリーンリーダーは「ボタンを押したら、どこに何が表示され
    たのか」をユーザーに伝えることができません。 これらの課題を解決するために、WAI-ARIAを使ってコードに正しい「意味」を与えていきましょう。

解決策:「良い例」WAI-ARIAで意味付けしたコード

上記の3つの課題(役割・状態・関連性)を、WAI-ARIAを使って解決したのが次のコードです。変更点に▼▼▼マークを付けています。

 <!-- 良い例:WAI-ARIAで改善したコード -->
   <div>
    <!-- ▼▼▼ 変更点1: role="tablist" と aria-label を追加 ▼▼▼ -->
    <div role="tablist" aria-label="コードの悪い例と良い例" >
        <!-- ▼▼▼ 変更点2: role, aria-selected, aria-controls, tabindex を追加 ▼▼▼ -->
        <button role="tab" id="tabBad" class="tab-btn active" aria-selected="true" aria-controls="badExample">悪い例</button>
        <button role="tab" id="tabGood" class="tab-btn" aria-selected="false" aria-controls="goodExample" tabindex="-1">良い例</button>
    </div>
    <!-- ▼▼▼ 変更点3: role と aria-labelledby を追加 ▼▼▼ -->
    <div role="tabpanel" id="badExample" aria-labelledby="tabBad">
        ...
    </div>
    <div role="tabpanel" id="goodExample" aria-labelledby="tabGood" style="display:none;">
        ...
    </div>
</div>

<!-- aria-selected属性の状態を管理するため、JavaScriptも更新します。 -->

<script>
    const tabBad = document.getElementById('tabBad');
    const tabGood = document.getElementById('tabGood');
    const badExample = document.getElementById('badExample');
    const goodExample = document.getElementById('goodExample');

    function switchTab(selectedTab, unselectedTab, selectedPanel, unselectedPanel) {
        // スタイルのためのクラスを切り替え
        selectedTab.classList.add('active');
        unselectedTab.classList.remove('active');
        // ▼▼▼ 変更点4: aria-selected 属性を更新 ▼▼▼
        selectedTab.setAttribute('aria-selected', 'true');
        unselectedTab.setAttribute('aria-selected', 'false');

        // ▼▼▼ 変更点5: tabindex を管理してキーボード操作性を向上 ▼▼▼
        selectedTab.removeAttribute('tabindex');
        unselectedTab.setAttribute('tabindex', '-1');

        // パネルの表示/非表示
        selectedPanel.style.display = 'block';
        unselectedPanel.style.display = 'none';

    }

    tabBad.addEventListener('click', () => {
        switchTab(tabBad, tabGood, badExample, goodExample);

    });

    tabGood.addEventListener('click', () => {
        switchTab(tabGood, tabBad, goodExample, badExample);

    });
</script>

各属性の詳しい解説

なぜこれらの属性を追加することでアクセシビリティが向上するのか、一つずつ見ていきましょう。

  • role="tablist"
    • 役割: タブボタンを囲む親要素に付け、「これがタブ全体のリストですよ」と宣言します。スクリーンリーダーはこれを認識すると「タブ」と読み上げ、ユーザーにUIの役割を伝えます。
    • aria-label: tablist にラベルを付けます。複数のタブリストがページ内にある場合などに、これが何のタブリストなのかを区別するのに役立ちます。
  • role="tab"
    • 役割: 各ボタンに付け、「これがリストの中の一つ一つのタブですよ」と宣言します。
  • aria-selected="true/false"
    • 役割: どのタブが現在選択されているか、という「状態」を明確に示します。trueが選択中、falseが非選択です。
      これは見た目を変えるCSSのactiveクラスとは別に、スクリーンリーダーに状態を伝えるための非常に重要な属性です。必ずJavaScriptで状態を更新する必要があります。
  • aria-controls="ID"
    • 役割: タブとそのタブが開くコンテンツパネルを「関連付け」ます。aria-controlsの値には、対応するパネルの
      idを指定します。これにより、スクリーンリーダーは「このボタンを押すと、あのコンテンツが表示される」という関係性を理解できます。
  • role="tabpanel"
    • 役割: 各コンテンツのコンテナに付け、「これがタブによって表示されるパネルですよ」と宣言します。
  • aria-labelledby="ID"
    • 役割: パネルとそのパネルを制御するタブを逆方向に関連付けます。パネル自身に「自分のラベル(名前)はこのIDを持つタブですよ」と教える役割があります。
  • tabindex="-1"
    • 役割: キーボードのTabキーでフォーカスが当たる順番を制御します。-1を指定すると、その要素はTabキーでのフォーカス対象から外れます(ただしJavaScriptからのフォーカスは可能です)。非選択のタブに設定することで、ユーザーはTabキーでアクティブなタブだけに移動でき、キー操作が快適になります。
    これらの属性を追加することで、見た目はそのままでも、支援技術に対してUIの構造と状態を正確に伝えることができるようになり、誰にとっても使いやすいタブUIが完成します。

【重要】ARIAは見た目を変えない

aria-hidden="true"と書いても、要素は画面上から消えませんaria-disabled="true"と書いても、ボタンはクリックできます。ARIAはあくまで支援技術に「意味」を伝えるだけです。実際の見た目や動作は、CSSやJavaScriptで別途実装する必要があります。

まずは1つのコンポーネントからARIAを育ててみよう

理論は分かっても、実践しないと身につきません。
まずは、あなたのサイトにある最も身近なカスタムUI(例えば、アイコンだけのボタンや、JavaScriptで実装したアコーディオン)を一つだけ選んでみましょう。

そして、

  1. そのUIに最適なroleは何か?
  2. aria-labelで、より分かりやすい説明を加えられないか?
  3. 開閉や選択の状態は、aria-expandedaria-selectedで示せないか? と考えてみてください

ChromeのDevToolsにある「アクセシビリティ」タブや、スクリーンリーダー(WindowsならNVDA、MacならVoiceOverが無料で使えます)を起動して、ARIAを追加する前と後で読み上げ方がどう変わるかを確認するのが、最良の学習方法です。 小さな一歩が、あなたのサイトをより多くの人にとって使いやすい、質の高いサイトへと成長させます。

✉️ あなたのリクエスト教えてください!

「この記事わかりやすかった!」
「他にもこんな記事書いてほしい!」
そんな声を、ぜひXのDMで教えてください!😊📩

できるだけリクエストにお応えして、今後の記事作成に活かしていきます

▶️ X@TumaLOVE0127もフォローしてもらえるとめちゃくちゃ嬉しいです!

hisa

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA