このスライドは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
使い方
<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が一意になった
-
useId
指定すればアプリケーション全体で被らない! - ハイドレーションミスマッチもなくなる!
-
app.config.idPrefix
でプレフィックスの変更が可能!app.config.idPrefix = 'my-app'
-
my-app-1
のように生成される
Nuxtでも使用できたuseId
- Nuxt 3.10から登場(Vue.jsよりも先)
- Vue3.5から
useId
が登場してから内部処理がVue.jsのものに置き換わった
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が出力される - アクセシブルなUIを作るために有用
- SSRフレンドリーなのでぜひ活用してみてね