Reactで作る、WAI-ARIA対応のアクセシブルなタブ型UI

47
26

この記事は『WAI-ARIA対応のアクセシブルなタブ型UI(基本編)』の続きです。

WAI-ARIAはアクセシビリティーの改善に役立つと先の記事で紹介しました。この記事ではWAI-ARIAに対応したReactでのタブのユーザーインタフェースを解説します。

サンプルをGitHubにアップしているので、デモとソースコードをご覧ください。

※本記事はViteで開発環境を用意し、React 19で説明しています。

Reactでのステート管理

実装の要素としてstateに選択されたタブのIDを保持することとします。

▼ステート定義の部分の抜粋

// タブの選択状態をステート管理
const [state, setState] = useState<string>("1");

※コードは抜粋で掲載しているので、コピーする際はGitHubの「App.tsx」を参照ください。

HTMLの実装

HTMLの実装を紹介します。このコードはJSXで書いています。

▼JSXの部分の抜粋

<div>
  <ul role="tablist">
    {CONTENT_LIST.map((tab) => (
      <li key={tab.id} role="presentation">
        <button
          role="tab"
          id={toTabId(tab.id)}
          aria-controls={toPanelId(tab.id)}
          aria-selected={state === tab.id}
          onClick={handleClick}
        >
          {tab.label}
        </button>
      </li>
    ))}
  </ul>
  {CONTENT_LIST.map((tab) => (
    <div
      key={tab.id}
      role="tabpanel"
      id={toPanelId(tab.id)}
      aria-describedby={toTabId(tab.id)}
      hidden={state !== tab.id}
    >
      {tab.content}
    </div>
  ))}
</div>

HTMLコーディングのポイントとしては次の通りです。

①期待どおりに読み上げられるようにrole属性を適切に利用します

タブとして機能するように、ul要素にrole="tablist"、 タブ部分となるbutton要素にrole="tab"、 パネル部分のdiv要素にrole="tabpanel"を追加します

<ul role="tablist"></ul>
<div role="tabpanel" …></div>

慣習にしたがってul>liでマークアップしましたが、読み上げの支障となるのでliタグにはrole="presentation"を指定してます(もしかしたらタブUIにul>liを使う必要はないかもしれません)

②タブ側のボタンはa要素ではなくbutton要素を使ってます。

<button role="tab" …></button>

macOS Safariだとaタグはデフォルトでタブキーで操作できないためです。Safariでは次のように「Tabキーを押したときにウェブページ上の各項目を強調表示」を選択すると、a要素もタブキーで選択可能になります。ただ、このオプションを設定していなくてもタブキーで選択されたほうが望ましいと考えてのことです

macOS Safariでの設定ウインドウの「Tabキーを押したときにWebページ上の各項目を強調表示」

JavaScriptとReactに絡んでくるポイント

  • タブとなるbutton要素とパネルのdiv要素の関連性を示すためaria-controls属性を指定します。値は任意でid属性を指定します
  • button要素にタブの選択状態を伝えるために、aria-selectedを真偽値で指定します
    • Reactのステートの値で動的とします。こうすれば半自動的にaria-selected属性が切り替わります
  • パネル部分が表示・非表示の状態を伝えるために hidden属性を真偽値で指定します

またReactでは、id属性を生成するためのuseIdフックを利用しています。useIdフックは各コンポーネントで一意のIDを生成するためのフックです。これを利用することで、タブボタンやタブパネルに固有のid属性を付与しやすくなります。

const id = useId();

タブボタンやタブパネルのid属性は扱いやすくするために変換用の関数を用意しました。

const toPanelId = (panelId: string) => id + "-panel-" + panelId;
const toTabId = (tabId: string) => id + "-tab-" + tabId;

ボタン要素のイベントハンドラー

ボタン要素のイベントハンドラーのコードを紹介します。ボタンとパネルの紐付けは、意味的に合致している aria-controls 属性を利用してます。JavaScriptの制御が必要なものは独自の変数ではなく、可能な限り aria-* 属性で代替するのがベターなやり方と思います。なお、React Hooksを使って書いています。

▼イベントハンドラーの部分の抜粋

// タブの選択状態をステート管理
const [state, setState] = useState<string>("1");

// クリックしたときのイベントハンドラーです。
const handleClick = (event: React.MouseEvent) => {
  // イベント発生源の要素を取得
  const element = event.currentTarget;

  // aria-controls 属性の値を取得
  const tabState = element.getAttribute("aria-controls");

  if (!tabState) {
    return;
  }

  const tabId = tabState.split("-").pop();
  if (!tabId) {
    return;
  }

  // プロパティーを更新
  setState(tabId);
};

CSSの実装

CSSはなるべく class 属性を使わず、aria-* 属性をセレクターとして指定しています。こうすれば、余計なクラス属性を増やす必要がなくなります。

/* UI制御のための指定 */
[aria-selected="true"] {
  background-color: royalblue;
  color: white;
}

※コードは抜粋で掲載しているので、コピーする際はGitHubの「App.css」を参照ください。

CSSの実装はCodeGridの記事「WAI-ARIAを活用したフロントエンド実装」で紹介されている「aria属性をCSSセレクターとして利用する」「独自に名前を付けるくらいなら、意味的に合致するaria属性を利用して、アクセシビリティーを確保しましょう」の提案をアイデアとしています。

まとめ

Reactで実装する場合はタブの状態はいずれかのstatepropsで管理しているはずです。その値を間借りしてaria-*属性に適用すれば、簡単にアクセシビリティーを向上できます。これは一例に過ぎません。さまざまなユーザーインターフェイスに利用できるので応用くださいませ。

昨年の記事「脱jQueryのためにしたこと」でも紹介したように、これらのJSライブラリとWAI-ARIAの相性は抜群です。Reactユーザーがほんの少しWAI-ARIAの理解が進めば、簡単に利用できるでしょう。この記事によって、音声読み上げを求めているエンドユーザーへの配慮が少しでも進めばと考えています。

この方法はAngularやVue.jsでも実装できます。詳しくは次の記事を参照ください。

補足

記事を作成するにあたり複数のサンプルを用意して音声読み上げソフト(macOSの「VoiceOver」や「NVDA日本語版」)で検証しました。

※この記事が公開されたのは7年前ですが、今月5月に内容をメンテナンスしています。

池田 泰延

ICS代表。筑波大学 非常勤講師。ICS MEDIA編集長。個人実験サイト「ClockMaker Labs」のようなビジュアルプログラミングとUIデザインが得意分野です。

この担当の記事一覧