📣

Nuxt v3.12 からは NuxtRouteAnnouncer でアクセシビリティ改善に取り組もう

2024/06/26に公開2

はじめに

先日、Nuxt v3.12 がリリースされました。
リリースノートの中に <NuxtRouteAnnouncer /> という気になるコンポーネントに関する記述があったため、調べてみることにしました。

https://nuxt.com/docs/api/components/nuxt-route-announcer

公式サイトの翻訳

まず、<NuxtRouteAnnouncer /> の概要を理解するために日本語訳をしようと思います。
以下、公式サイトの日本語訳です。

使い方

<NuxtRouteAnnouncer/>app.vue または layouts/ に追加するとページタイトルの変更を支援技術に知らせることでアクセシビリティを向上させることができます。これにより、スクリーンリーダーを利用するユーザーに対して、ナビゲーションの変更を確実に知らせることができます。

<template>
  <NuxtRouteAnnouncer />
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

Slots

カスタム HTML またはコンポーネントをルートアナウンサーのデフォルトのスロットとして渡すことができます。

  <template>
    <NuxtRouteAnnouncer>
      <template #default="{ message }">
        <p>{{ message }} was loaded.</p>
      </template>
    </NuxtRouteAnnouncer>
  </template>

Props

  • atomic: スクリーンリーダーが変更のみを読み上げるか、コンテンツ全体を読み上げるかを制御します。更新時に全てのコンテンツを読み上げる場合は true を設定し、変更のみを読み上げる場合は false を設定します。(初期値 false)
  • politeness: スクリーンリーダーによる読み上げの緊急度を設定します。: off (読み上げを無効)、polite (ユーザーがアイドル状態になるまで待つ)、または assertive (現在の読み上げを中止し、即座に読み上げられる)。 (初期値 polite)

このコンポーネントは任意です。
完全なカスタマイズを実現するためには、ソースコードに基づいてカスタムの設定を実装できます。

useRouteAnnouncer コンポーザブルを使用して
これにより、カスタムのメッセージを読み上げるように設定できます。

アクセシビリティについて

なぜ、アクセシビリティを考慮する必要があるのでしょうか?
<NuxtRouteAnnouncer /> を追加し、ページのタイトルが変更された時に変更内容が読み上げられるとどんな良いことがあるのでしょうか?

ページタイトルに限らず、アプリ内のコンテンツを一度読み込みを終えた後にアプリ内のコンテンツが更新された際に読み上げが行われないとスクリーンリーダーを使用するユーザーはコンテンツの変更に気づくことができません。

もし、変更内容がユーザーにとって重要な情報であった場合でも読み上げが行われなければ、新しい情報に気づくことはできず、古い情報のままアプリを操作してしまう可能性があります。

アプリ内のコンテンツが更新された時に、ユーザーに対して変更内容を通知することでこれらの課題を解決することができます。

ただし、更新された内容が全て重要な情報であるとは限りません。

ユーザーにとって重要な変更を必要なタイミングでユーザーに通知することでアクセシビリティを向上させることができます。

<NuxtRouteAnnouncer /> は、ページのタイトルの変更を通知する機能です。

ページタイトル以外のコンテンツが変更した時に通知を行いたい場合は、該当要素に aria-livearia-atomic を設定してください。

https://developer.mozilla.org/ja/docs/Web/Accessibility/ARIA/Attributes/aria-live

https://developer.mozilla.org/ja/docs/Web/Accessibility/ARIA/Attributes/aria-atomic

検証

実際に NuxtRouteAnnouncer を設定した状態でスクリーンリーダーを使用してページタイトルの変更を読み上げてみようと思います。

事前準備

Nuxt v3.12 のプロジェクトを用意します。
環境構築について本記事では触れません。
新規でプロジェクトを作成する際は、公式サイトを参照してください。

https://nuxt.com/docs/getting-started/installation#new-project

検証時に使用した v3.12.2 では、app.vue<NuxtRouteAnnouncer /> が追加された状態でプロジェクトが作成されました。

<template>
  <div>
    <NuxtRouteAnnouncer />
    <NuxtWelcome />
  </div>
</template>

もし、<NuxtRouteAnnouncer />app.vue に存在しない場合や layouts/ に記述したい場合はご自身で <NuxtRouteAnnouncer /> を追加してください。

今回、検証時には app.vue とは別にページ用のファイルを作成するため、 <NuxtWelcome /><NuxtPage /> に変更します。

<template>
  <div>
    <NuxtRouteAnnouncer />
    <NuxtPage />
  </div>
</template>

次に pages/index.vue を作成し、下記の内容を記述します。

<script setup lang="ts">
/**
 * @typedef ユーザー情報
 * @property {string} name - 名前
 * @property {number} age - 年齢
 */
type User = {
  user: {
    name: string;
    age: number;
  }
}

const loading = ref<boolean>(false);
const name = ref<string>('');

/*
 * ボタンがクリックされたらユーザー情報を取得する
 */
const handleClickButton = async () => {
  loading.value = true;

  const { data } = await useFetch<User>('/api/user');

  if (data.value?.user?.name) {
    name.value = data.value.user.name;
  }

  loading.value = false;
}

useHead({
  title: () => loading.value
    ? 'loading...'
    : 'home'
})
</script>

<template>
  <div>
    <button type="button" @click="handleClickButton">名前を取得する</button>
  </div>
</template>

最後にボタンをクリックした時にユーザー情報を1.5秒後に返すAPIを作成します。
server/api/user.ts を作成し、次の内容を追加します。

export default defineEventHandler(async() => {
  // ユーザー情報を定義
  const user = {
    name: 'John',
    age: 18
  };

  // 値の返却までに1.5秒待機
  await new Promise(resolve => setTimeout(resolve, 1500));

  return {
    user
  }
});

事前準備は以上です。
処理の概要を説明すると、「名前を取得する」というボタンを押すとページタイトルが「loading...」に変更され、1.5秒後に「home」というページタイトルへ再度変更されるという処理を実装しました。

NuxtRouteAnnouncer で変更を読み上げる

事前準備を終えた状態でアプリを起動し、スクリーンリーダーをオンにした状態でアプリへアクセスし、「名前を取得する」というボタンを押してみます。

少し待つと変更内容が「loading...」と読み上げられます。
そして、続けて「home」と読み上げられました。

変更があった時にすぐ読み上げられるのではなく、今読み上げている内容を読み終えた後に変更内容が読み上げられるのは、<NuxtRouteAnnouncer />politeness という props の初期値が polite だからです。

politeness という props を設定することで読み上げのタイミングを変更することができます。
この politeness は最終的に aria-live 属性に設定されます。

https://developer.mozilla.org/ja/docs/Web/Accessibility/ARIA/Attributes/aria-live

politeness に設定することができる値は、以下の3つの内いずれかです。

  • assertive - 即座に通知されます
  • polite - 読み上げ中の文章を読み終えたタイミングやユーザーの操作が中断し、アイドル状態になったタイミングで読み上げを行います
  • off - 対象の要素にユーザー自身がフォーカスしない限り、読み上げが行われない

これらの値は、aria-live で設定できる値と同様です。
<NuxtRouteAnnouncer /> の実装を追うと上記の3つで型定義されていることが分かりますね。

https://github.com/nuxt/nuxt/blob/c87ca8607cf5dde72979c44abbd0da818d608078/packages/nuxt/src/app/composables/route-announcer.ts#L6-L11

変更があったらすぐに通知を行うために assertive を設定してみます。

<NuxtRouteAnnouncer politeness="assertive" />

ボタンを押した直後に「loading...」と通知されます。

そして、1.5 秒後に「home」と読み上げられました。

次に値を off に変更してみます。

<NuxtRouteAnnouncer politeness="off" />

この状態でボタンを押してしばらく待っても読み上げが行われません。
off は、該当する要素にフォーカスしない限りは読み上げが行われないからです。

読み上げ内容を変更する

atomic という props を設定することで読み上げの内容を変更することができます。
これは、aria-atomic に相当する機能です。
この atomic は最終的に aria-atomic 属性に設定されます。

https://developer.mozilla.org/ja/docs/Web/Accessibility/ARIA/Attributes/aria-atomic

atomic に設定することができる値は、true または false です。
デフォルトの値は、false になっています。

値が true の場合、読み上げはライブリージョン領域を全て読み上げます。
逆に false の場合、変更箇所のみが読み上げられます。

app.vue を次のように変更します。

<template>
  <div>
    <NuxtRouteAnnouncer politeness="assertive">
      <template #default="{ message }">
        <p>ページタイトルが変更されました。</p>
        <span>{{ message }}</span>
      </template>
    </NuxtRouteAnnouncer>
    <NuxtPage />
  </div>
</template>

デフォルト値は false になっているため、ページタイトルが変更された際に「home」もしくは「loading...」だけが読み上げられます。

次に値を true に変更し、読み上げの対象がライブリージョン領域の全体になっていることを確認します。

<NuxtRouteAnnouncer :atomic="true" politeness="assertive">

ボタンを押した直後に「ページタイトルがに変更されました。loading...」と通知されます。

そして、1.5 秒後に「ページタイトルがに変更されました。home」と読み上げられました。

まとめ

以上が、<NuxtRouteAnnouncer /> コンポーネントについてでした。
ページタイトルが変更された時に通知を行いたい場合は、ぜひ<NuxtRouteAnnouncer />を活用し、アクセシビリティを向上させていきましょう。

おまけ

せっかくなので <NuxtRouteAnnouncer /> の使い方だけでなく、<NuxtRouteAnnouncer /> がどのようにページタイトルの読み上げを実現しているのか、実装を簡単に追ってみようと思います。

packages/nuxt/src/app/components/nuxt-route-announcer.ts<NuxtRouteAnnouncer /> のソースコードがあります。

特に注目したい点が return 箇所です。

https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/components/nuxt-route-announcer.ts#L24-L46

h関数を使用して、span タグを生成していることが分かります。

実際にアプリを起動した状態で DevTools を使用して確認すると、確かに span タグが生成されています。

span タグには、props で指定した atomicpoliteness の値が、aria-atomicaria-live の値として指定されています。

つまり、この span タグ内のコンテンツが変更された時に、ユーザーに対して通知が行われるという仕組みです。

ページタイトルと聞くと title タグのことかな?と想像してしまうのですが、読み上げの対象となるライブリージョン領域は、コンテンツ内に存在しているんですね。

推測にはなってしまいますが、title タグ自体に aria-atomicaria-live などの WAI-ARIA を直接指定することができないため、上記のような実装を行なっているのではないかと思います。

GitHubで編集を提案

Discussion

yamanokuyamanoku

気になった点があったのでコメント失礼します 🙇

理由は当然と言えば当然なのですが、<NuxtRouteAnnouncer /> の politeness という props の初期値が off だからです。

この直後のroute-announcer.tsのソースで示しているとおりデフォルトはpoliteになるためoffではないと思います

export type NuxtRouteAnnouncerOpts = {
  /** @default 'polite' */
  politeness?: Politeness
}

いつまで待っても変更内容が読み上げられません。

aria-liveの値がassertiveではないのですぐには通知されませんが、デフォルトのpoliteであれば一通り読み上げが終わった後に通知が届くはずです(検証されているmacOS環境であれば 💻)

Hikaru KobayashiHikaru Kobayashi

yamanokuさん、コメントありがとうございます!
おっしゃる通り、politeness の初期値は off ではなく polite なので読み上げは行われますね。
改めて手元でも検証したところちゃんと通知されました。
誤った情報を記載しており、大変失礼いたしました。
本記事の内容も修正させて頂きました!