LoginSignup
32
8

More than 1 year has passed since last update.

React 18のuseIdでアクセシビリティ向上(WAI-ARIAのRelationship attributes)

Last updated at Posted at 2022-06-05

この記事の概要

React 18で新しくuseIdというhooksが使えるようになりました。
コンポーネントにWAI-ARIAを導入するにあたって、便利になると思ったのでいくつかのパターンを紹介します。

また、Qiita Engineer Festa 2022の「React 18、あなたならどう使いこなす?」への投稿記事でもあります。

記事を書こうと思った理由——useId登場以前の迷い

実例を紹介する前に、なぜこの記事を書こうと思ったかについて説明します。

例えばaria-labelledbyを使う場合、以下のようになります。
(コードはMDN Docsのaria-labelledbyのページより拝借)

<span role="checkbox" aria-checked="false" tabindex="0" aria-labelledby="tac"></span>
<span id="tac">I agree to the Terms and Conditions.</span>

簡単に説明すると、まずrole="checkbox"のspanはaria-labelledby="tac"の記述によってid="tac"のspanを参照しています。
そのためスクリーンリーダー使用時にrole="checkbox"のspanにフォーカスを当てるとI agree to the Terms and Conditions.と読み上げられます。

aria-labelledbyが無かった場合、見た目としては紐付いていてもスクリーンリーダーはそれを検知できません。
マシンからしたら、謎のチェックボックスがぽつんと置いてある感じ……とでも言えるのでしょうか。

aria-labelledbyも含めて、この後紹介する属性はすべて他要素のidを参照して読み上げなどを支援します。

ここで、idを使用するため若干の難しさが生まれます。
コンポーネントを作るにあたって、どの場所で何回使われるか?は考慮に入れるべきではありません。
しかしidは1ページで1つしか使ってはいけないので、単に固定した文字列を指定すると、他要素と重複してしまう可能性があります。

回避するために、propsとして毎回idを指定する、nanoidなどのライブラリを使用する、といった選択肢が挙げられるでしょう。
前者は結局人間が指定するのでうっかり重複してしまうかもしれませんし、後者は(気持ちの問題ですが)このために依存関係を増やすのは……となっていました。

上記のような悩みがあったため、React自体がユニークなidを生成する仕組みを提供してくれたのはありがたいです。

useIdを使う

useIdを使う場合、以下のように書きます。(ほぼReact公式ドキュメントのuseIdの説明のままです。)
特に迷うことはありませんね。

import { useId } from "react";

export function Checkbox() {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>Do you like React?</label>
      <input id={id} type="checkbox" name="react" />
    </>
  );
}

ここから、2つの例を紹介します。
基本はMDN DocsにあるHTMLのコードをReactに置き換えて記載しています。

1つのコンポーネントに1つのidで済むパターン

上で書いた内容、ほぼそのままです。

import { useId } from "react";

export function Checkbox({ description }) {
  const id = useId();
  return (
    <>
      <span
        role="checkbox"
        aria-checked={isChecked} // 何かしらでコントロール。記事の内容とは直接関係ないので省略。
        tabIndex={0}
        aria-labelledby={id}
      ></span>
      <span id={id}>{description}</span>
    </>
  );
}

例ではaria-labelledbyを使いましたが、書き方はaria-describedbyaria-detailsaria-ownsなども同じです。

参考:

1つのコンポーネントに複数のidが必要なパターン

inputコンポーネントを作るとして、label要素とエラーメッセージもセットにしておきたいものです。
すると、labelのhtmlForで指定するidと、inputのaria-errormessageで指定するためのidが2つ必要になります。

このように1つのコンポーネント内で複数のidが必要な場合はid + 接尾辞の形式をとってください。
これもReact公式ドキュメントのuseIdの説明に記載があります。

と言っても、2回以上useId()を呼んでもエラーなどは出ません。
パフォーマンスなどの問題があるのでしょうか……調べきれなかったため、もし分かる方がいたら是非コメントで教えてください。

import { useId } from "react";

export function Input({ label, errorMessage, ...props }) {
  const id = useId();
  return (
    <p>
      <label htmlFor={`${id}-input`}>{label}</label>
      <input
        type={props.type}
        name={props.name}
        id={`${id}-input`}
        aria-invalid={!!errorMessage}
        aria-errormessage={`${id}-errorMessage`}
      />
      {!!errorMessage && <span id={`${id}-errorMessage`}>{errorMessage}</span>}
    </p>
  );
}

もう少し複雑なパターンだと、タブリストのUIなんかがあるでしょう。
このままでは全然役に立たなそうなコンポーネントですが、イメージは掴めると思うので例として示します。

import { useId } from "react";

export function TabList({ firstTab, secondTab }) {
  const id = useId();
  return (
    <div>
      <div role="tablist" aria-label="Sample Tabs">
        <span
          role="tab"
          aria-selected={isSelected} // 何かしらでコントロール。記事の内容とは直接関係ないので省略。
          aria-controls={`${id}-panel-1`}
          id={`${id}-tab-1`}
          tabIndex={tabIndex} // 何かしらでコントロール。記事の内容とは直接関係ないので省略。
        >
          First Tab
        </span>
        <span
          role="tab"
          aria-selected={isSelected} // 同上
          aria-controls={`${id}-panel-2`}
          id={`${id}-tab-2`}
          tabIndex={tabIndex} // 同上
        >
          Second Tab
        </span>
      </div>
      <div
        id={`${id}-panel-1`}
        role="tabpanel"
        tabIndex={0}
        aria-labelledby={`${id}-tab-1`}
        hidden={isHidden} // 同上
      >
        <p>{firstTab}</p>
      </div>
      <div
        id={`${id}-panel-2`}
        role="tabpanel"
        tabIndex={0}
        aria-labelledby={`${id}-tab-2`}
        hidden={isHidden} // 同上
      >
        <p>{secondTab}</p>
      </div>
    </div>
  );
}

参考:


最後まで読んでくださってありがとうございます!
Twitterでも情報を発信しているので、良かったらフォローお願いします!

32
8
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
8