7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vue3.5からのuseIdでアクセシブルな汎用コンポーネントを作ってみよう

Last updated at Posted at 2025-02-18
1 / 29

このスライドはUV Study : Vue.js LT会 ~業務で活きる実践的なVue~での発表資料です。


自己紹介


大山奥人(Okuto Oyama)

マイクを持って発表している大山奥人

今日は
Vue Fes Japan 2024
落選したプロポーザルの
供養に来ました


突然ですが質問です


アプリケーション内で
ID属性をすべて一意で
管理できる自信、ありますか?


たとえばこんなコンポーネントを作ったとします。

<script setup lang="ts">
const props = defineProps<{
  label: string
}>();
</script>

<template>
  <div>
    <label for="form-control">
      {{ props.label }}
    </label>
    <input id="form-control" type="text" />
  </div>
</template>

これ、複数で使うと
どうなるんだっけ?


<FormTextControl label="姓" />
<FormTextControl label="名" />

<div>
    <label for="form-control"></label>
    <input id="form-control" type="text">
</div>
<div>
    <label for="form-control"></label>
    <input id="form-control" type="text">
</div>

IDが一意じゃない!🤯


なぜ一意じゃないといけない?

HTML要素で指定される場合、id属性値は、要素のツリーですべてのIDに共通して一意でなければならず、かつ少なくとも1つの文字を含まなければならない。値は一切のASCII空白文字を含んではならない。
HTML Living Standard 3.2.6 グローバル属性


操作も期待したものではなくなってしまう…

姓と名のテキストフィールドが存在しており、名のラベル部分をクリックすると姓のテキストフィールドにフォーカスが当たってしまう

一意にするための
解決策を考えてみる


uuidを使用する

<script setup>
import { v4 as uuid } from "uuid";
const id = uuid();
// ex: 0523f051-09b4-49f5-a6e0-92fa62ead546
</script>
  • 基本衝突することはない
  • ただしライブラリ依存になってしまう

Web Crypto APIを使用する

<script setup>
const id = crypto.randomUUID();
</script>
  • ライブラリ依存ではなくなる
    • Node.js > 19から使える
  • ただしSSR時、ハイドレーションミスマッチとなる可能性あり
    • uuidのときも同様

Vue.js公式APIの useId

  • Vue3.5から追加された
  • 一意のIDを生成してくれる
  • SSRでも安全に使用できる
  • 似た仕組みをもつUIライブラリ

使い方

vue
<script setup lang="ts">
+ import { useId } from 'vue';
+ const formId = useId();
const props = defineProps<{
  label: string
}>();
</script>

<template>
  <div>
-    <label for="form-control">{{ props.label }}</label>
-    <input id="form-control" type="text" />
+    <label :for="formId">{{ props.label }}</label>
+    <input :id="formId" type="text" />
  </div>
</template>

<FormTextControl label="姓" />
<FormTextControl label="名" />

<div>
  <label for="v-0"></label>
  <input id="v-0" type="text">
</div>
<div>
  <label for="v-1"></label>
  <input id="v-1" type="text">
</div>

IDが一意になった :thumbsup_tone2:

  • useId指定すればアプリケーション全体で被らない!
  • ハイドレーションミスマッチもなくなる!
  • app.config.idPrefixでプレフィックスの変更が可能!
    • app.config.idPrefix = 'my-app'
    • my-app-1のように生成される

Nuxtでも使用できたuseId


ID属性は
アクセシビリティを向上させる
UIコンポーネント作成に
活用できる


様々なパターンで
useIdを適応してみる


これから提示するパターンはあくまでも仕様に沿った例であり、具体的な操作に関する部分は省略しています。
実際に使用する際はブラウザ上でアクセシビリティチェックをして問題なく期待した動きになるかを確認してください。


フォームの詳細な説明

<script setup lang="ts">
import { useId } from 'vue';
const fromId = useId();
const hintId = useId();
const props = defineProps<{
  label: string,
  hintMessage: string,
}>();
</script>

<template>
  <div>
    <label :for="id">
      {{ props.label }}
    </label>
    <input 
      :id="id"
      type="text"
      :aria-describedby="hintId"
    />
    <p :id="hintId">
      {{ props.hintMessage }}
    </p>
  </div>
</template>

入力するフォームのヒント、例えばパスワードでは何文字以上・半角英数字込みなのかなどの情報を載せることがあるので、それと関連付けさせる。


タブUIでのタブとタブパネル紐づけ

<script setup lang="ts">
import { useId } from 'vue';

import Tab from './Tab.vue';
import TabPanel from './TabPanel.vue';

defineProps<{
  tabs: { title: string; content: string }[];
}>();

const tabId = useId();
const tabPanelId = useId();
</script>

<template>
  <div>
    <Tab
      v-for="(tab, index) in tabs"
      :id="`${tabId}-${index}`"
      :key="tab.title"
      :aria-controls="`${tabPanelId}-${index}`"
    >
      {{ tab.title }}
    </Tab>
  </div>
  <div>
    <TabPanel
      v-for="(tab, index) in tabs"
      :id="`${tabPanelId}-${index}`"
      :key="tab.content"
      :aria-labelledby="`${tabId}-${index}`"
    >
      <div>{{ tab.content }}</div>
    </TabPanel>
  </div>
</template>

現在表示されているタブパネルの内容が操作するタブと紐づいているかを関連付ける。
※TabとTabPanelの実装については省略


モーダルダイアログの説明

<script setup lang="ts">
import { useId } from 'vue';
const modalHeadlineId = useId();
const modalDescriptionId = useId();
const props = defineProps<{
  headline: string,
  description: string,
}>();
</script>

<template>
  <dialog
    :id="modalId"
    :aria-labelledby="modalHeadlineId"
    :aria-describedby="modalDescriptionId"
  >
    <div>
      <h2 :id="modalHeadlineId">
        {{ props.headline }}
     </h2>
      <p :id="modalDescriptionId">
        {{ props.description }}
      </p>
    </div>
  </dialog>
</template>

どういったモーダルダイアログが表示されているかを説明するために紐付ける。
詳細な説明文がある場合はaria-describedbyで関連させる。


UIライブラリでのuseId活用

ライブラリによっては独自のuseIdを実装していたが、Vue.jsのuseIdを使用するように非推奨にしていたりします。


まとめ

  • useIdを使うと一意のIDが出力される :tada:
  • アクセシブルなUIを作るために有用 :muscle_tone2:
  • SSRフレンドリーなのでぜひ活用してみてね :raised_hands_tone2:
7
4
0

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
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?