Nuxt

NuxtRouteAnnouncerの使い方|props・読み上げられない原因とNuxtAnnouncerとの使い分け

NuxtRouteAnnouncerは、クライアントサイドのルート遷移が起きたことをスクリーンリーダーへ伝えるためにNuxtが用意した組み込みコンポーネントです。Nuxt 3.12.0(2024年6月11日公開)から本体に同梱されており、追加パッケージのインストールは要りません。この記事では公式ドキュメントとNuxt本体のソースコードをもとに、配置場所、politenessatomicが実際に出力するHTML、読み上げが起きないときの原因、そしてNuxt 4.4.2で追加されたNuxtAnnouncerとの役割分担を整理します。

まとめ

  • NuxtRouteAnnouncerはNuxt 3.12.0以降のコアに同梱。nuxi initが生成するapp.vueにも最初から書かれており、モジュール追加は不要。
  • 読み上げる文言はページの<title>そのもの。ルート間でtitleが同じ、または未設定なら何も読み上げられない。
  • politenessは出力されるrolearia-liveを切り替える(polite→role="status"、assertive→role="alert"、off→role属性なし)。atomicの既定値はfalse
  • フォーム検証やトーストなど遷移を伴わない読み上げはNuxtAnnounceruseAnnouncerの担当(Nuxt 4.4.2以降)。NuxtRouteAnnouncerで代用するとページ再描画のたびに文言が上書きされる。
  • アナウンサーを置いてもフォーカスは移動しない。スキップリンクなどの手当ては別途必要。

以下、出力されるDOMの実体から順に見ていきます。

NuxtRouteAnnouncerが埋める「ページが変わったことが伝わらない」問題

ハイドレーション後の遷移は支援技術に通知されない

ブラウザがドキュメントを読み込み直したとき、スクリーンリーダーは新しいページの読み込みを自力で検知して読み上げます。ところがNuxtとは?Vue.jsとの違い・レンダリング方式・採用判断まで実装視点で解説【2026年7月版】で扱っているとおり、Nuxtアプリはハイドレーションが済んだあとクライアント側でルーティングします。ドキュメントは再読み込みされないため、ブラウザは新しいページの通知もフォーカスのリセットも行いません。リンクを押した利用者には、画面が差し替わったという手がかりが何も残らない状態になります。

この穴を埋めるのがNuxtRouteAnnouncerです。公式のアクセシビリティガイドは、この仕組みを「隠しライブリージョンをレンダリングし、遷移のたびに新しいページタイトルをそこへ書き込む」ものと説明しています。SPA・SSR・SSGでこの前提がどう変わるかはレンダリングとは?ブラウザ描画の仕組みとCSR/SSR/SSGの違い・選定基準を解説で整理しています。

実際に出力されるDOMとaria属性

コンポーネントは次の構造を1つだけ描画します。外側のspan.nuxt-route-announcerposition: absoluteで浮かせ、内側のspanをクリップして視覚的に消したうえで、支援技術からは読める状態に保つ形です。内側のstyleはソースにある11プロパティをそのまま出力します。

<span class="nuxt-route-announcer" style="position: absolute;">
  <span role="status" aria-live="polite" aria-atomic="false"
        style="border: 0; clip: rect(0 0 0 0); clip-path: inset(50%);
               height: 1px; width: 1px; overflow: hidden; position: absolute;
               white-space: nowrap; word-wrap: normal; margin: -1px; padding: 0;">
    会社概要 | 一創
  </span>
</span>

クラス名が固定なので、ブラウザの開発者ツールで.nuxt-route-announcerを検索すれば設置できているかを即座に判定できます。ここで使われているaria-livearia-atomicroleの意味はWAI-ARIAとは何か?その概要と目的をわかりやすく解説で扱っています。

導入手順とtitle設計

app.vueまたはlayouts配下に1つ置く

導入はコンポーネントを1つ書くだけです。パッケージのインストールやnuxt.configへの追記は必要ありません。公式スターター(nuxi initが生成するプロジェクト)のapp.vueには、Nuxt 3系・Nuxt 4系のどちらのブランチでも最初から<NuxtRouteAnnouncer />が書かれています。既存プロジェクトでapp.vueを自作している場合だけ、次のように追記します。

<!-- app/app.vue -->
<template>
  <NuxtRouteAnnouncer />
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

Nuxt 4ではソースディレクトリがapp/配下へ移ったため、パスはapp/app.vueapp/layouts/になります。ディレクトリ構成の変更点はNuxt 4とは?インストール・最新バージョン確認・Nuxt 3との違いとPinia連携まで使い方を解説にまとめました。レイアウトごとに置き分ける必要はなく、アプリ全体で1か所に置けば足ります。アナウンサーの実体がNuxtアプリ単位で共有されるためです。

ページごとに異なるtitleを与える

アナウンサーはUnheadが描画したtitleを読むだけなので、titleの設計がそのまま読み上げの質になります。公式ガイドは「2つのルートが同じ<title>を共有していれば、その間を移動しても利用者には何も聞こえない」と明記しています。app.vueでテンプレートを定め、各ページが自分の部分を埋める形にしておくと取りこぼしが減ります。

<!-- app/app.vue -->
<script setup lang="ts">
useHead({
  titleTemplate: title => title ? title + ' | 一創' : '一創',
})
</script>
<!-- app/pages/about.vue -->
<script setup lang="ts">
useHead({ title: '会社概要' })
</script>

一覧ページのページ送りのように、URLだけが変わってtitleが変わらない画面は要注意です。「検索結果 2ページ目」のようにページ番号や件数までtitleへ入れておくと、遷移が音として区別できるようになります。

propsとslotで読み上げ方を調整する

politenessはroleとaria-liveを切り替える

politenessが受け取る値は'assertive' | 'polite' | 'off'の3つで、既定値はpoliteです。ソース上はこの値によってroleの分岐が決まり、assertiveならalertoffならrole属性そのものが付かず、それ以外はstatusになります。Nuxt本体のテストでは、既定のpolite[role="status"]aria-live="polite"が出ることが検証されています。assertive側は、同じrole算出ロジックを共有するNuxtAnnouncerのテストで[role="alert"]aria-live="assertive"が確認できます。

<NuxtRouteAnnouncer politeness="assertive" />

ルート遷移の通知にassertiveを使うと、利用者が読み上げ中の文章を毎回中断させることになります。遷移の通知は既定のpoliteのままにし、assertiveはエラーなど中断してでも伝えるべき場面に取っておくほうが実用的です。offはライブリージョン自体を無効化するので、一時的な切り分け以外で使う場面はほとんどありません。

atomicの既定値はfalse

atomicは出力されるaria-atomicの値になり、支援技術が変更部分だけを読むか、リージョン全体を読み直すかを決めます。NuxtRouteAnnouncerでの既定値はfalseです。読み上げ対象がtitle文字列1つだけなので、既定のままでも実害は出にくい設定です。スロットで文言を組み立て、その一部だけが差し替わる構成にしたときにtrueを検討します。

default slotで文言を組み立てる

デフォルトスロットにはmessageというスロットプロパティが渡されます。titleをそのまま読ませるのではなく、前後に文言を足したいときに使います。

<template>
  <NuxtRouteAnnouncer>
    <template #default="{ message }">
      <span>{{ message }} を読み込みました</span>
    </template>
  </NuxtRouteAnnouncer>
</template>

スロットで変えられるのは読み上げられるマークアップだけで、読み上げのきっかけ自体は変えられません。あくまでtitleが変わったタイミングで、その文字列を材料に組み立てた内容が読まれます。なお公式ドキュメントの例は<p>を使っていますが、スロットの中身は<span>の子として描画されるため、上の例では<span>に置き換えています。

useRouteAnnouncerで任意の文言を流す場合の制約

set・polite・assertiveの3メソッド

useRouteAnnouncerはNuxt 3.12.0で追加されたコンポーザブルで(ソースの@since表記も3.12.0)、コンポーネントが内部で使っているアナウンサーの実体を取り出します。返り値はmessagepolitenessの2つのRefと、set(message, politeness)polite(message)assertive(message)の3メソッドです。内部型には後始末用の_cleanupもありますが、戻り型がOmit<RouteAnnouncer, '_cleanup'>のため公開されません。公式ガイドは検索結果の件数通知を例に挙げています。

<!-- app/pages/search.vue -->
<script setup lang="ts">
const { set } = useRouteAnnouncer()
const { data: results } = await useFetch('/api/search')

watch(results, (results) => {
  set((results?.length ?? 0) + ' 件見つかりました')
})
</script>

設定した文言はDOM描画のたびにtitleへ戻る

ここが実装で最もつまずきやすい点です。アナウンサーの実体は生成時に次のフックを張っており、DOMが描画されるたびにdocument.titleを読み直してメッセージを上書きします。

function _updateMessageWithPageHeading () {
  set(document?.title?.trim(), politeness.value)
}

activeHead?.hooks?.hook('dom:rendered', _updateMessageWithPageHeading)

つまりset()で入れた独自の文言は、次にDOMが描画された時点でtitleへ戻ります。同じページ内でリアクティブな更新が続く画面ほど、書いたはずの文言が読まれない現象に当たります。恒久的に独自の文言を読ませたいなら、set()ではなくuseHeadのtitle側を書き換えるか、次章のuseAnnouncerへ移すのが正解です。

NuxtAnnouncer(Nuxt 4.4.2以降)との使い分け

遷移の通知と、遷移を伴わない通知は別コンポーネント

Nuxt 4.4.2(2026年3月12日公開)で<NuxtAnnouncer>useAnnouncerが追加され、フォーム検証・トースト・読み込み状態といった遷移を伴わない読み上げの受け皿ができました。公式ドキュメントも、この用途ではNuxtRouteAnnouncerではなくNuxtAnnouncerを使うよう案内しています。両者の違いは次のとおりです。

観点 NuxtRouteAnnouncer NuxtAnnouncer
読み上げる対象 ルート遷移 任意のコンテンツ変更
発火 遷移時に自動 polite() / assertive() を呼ぶ
文言の出どころ ページのtitle 開発者が指定
atomicの既定値 false true
利用可能バージョン Nuxt 3.12以降 Nuxt 4.4.2以降

両方を同時に置けます。app.vueに並べて書き、コンポーザブルは必要なページから呼び出します。

<!-- app/app.vue -->
<template>
  <NuxtAnnouncer />
  <NuxtRouteAnnouncer />
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>
<!-- app/pages/contact.vue -->
<script setup lang="ts">
const { polite, assertive } = useAnnouncer()
const form = reactive({ name: '', message: '' })

async function submitForm () {
  try {
    await $fetch('/api/contact', { method: 'POST', body: form })
    polite('お問い合わせを送信しました')
  } catch {
    assertive('エラー: 送信に失敗しました')
  }
}
</script>

NuxtRouteAnnouncerで代用すべきでない場面

ボタン操作の結果やバリデーションエラーをNuxtRouteAnnouncerで読ませる実装は、Nuxt 4.4.2以降では選ぶ理由がありません。前章のとおりdom:renderedのたびにtitleへ戻されるため、文言が読まれるかどうかが描画タイミング次第になるからです。useAnnouncer側のアナウンサーはtitleを監視せず、_cleanup()も文言を空へ戻すだけなので、開発者が指定した文言がそのまま残ります。

逆に、Nuxt 3系(現行の3系最新は3.21.10)に留まっているプロジェクトではNuxtAnnouncerを使えません。その場合はuseRouteAnnouncerset()ではなく、通知したい内容を含む形でtitleを書き換える、あるいは自前でaria-live領域を持つコンポーネントを用意するほうが挙動が安定します。公式ドキュメントもコンポーネントは省略可能で、ソースコードを土台に自作してよいと案内しています。

読み上げが起きないときの切り分け手順

スクリーンリーダー無しで検証する

まずは音ではなくDOMで確認します。ライブリージョンの中身が更新されているかどうかが分かれば、原因がNuxt側か支援技術側かを切り分けられます。ブラウザのコンソールで次を実行し、遷移させてみてください。

const el = document.querySelector('.nuxt-route-announcer span')

if (!el) {
  console.warn('NuxtRouteAnnouncer が描画されていません')
} else {
  new MutationObserver(() => console.log(el.textContent)).observe(el, {
    childList: true, characterData: true, subtree: true,
  })
}

遷移してもログが出ないならNuxt側の設定、ログは出るのに読み上げられないならスクリーンリーダーやOSの設定を疑います。実機での確認まで進むなら、NVDA・JAWS・VoiceOverのように読み上げエンジンごとにライブリージョンの扱いが異なるため、対象利用者の環境に合わせて選びます。

titleの重複・politeness=off・set()の上書きという3原因

  • ルート間でtitleが同じ、または未設定:ライブリージョンの文字列が変化しないため通知は発生しません。useHeadでページごとのtitleを設定します。
  • politeness="off"のままaria-live="off"が出力され、role属性も付きません。読み上げは起きない状態が正しい挙動です。
  • set()の文言が上書きされているdom:renderedフックがtitleを再設定します。独自文言はtitle側かuseAnnouncerへ移します。

初回のフルページロードでアナウンサーが鳴らないのは正常な動作です。ライブリージョンは初期表示の内容ではなく変化を通知する仕組みで、そもそも通常のページ読み込みはスクリーンリーダー自身が読み上げます。コンポーネント自体が描画されていないケースもあり、.nuxt-route-announcerが見つからない場合は、app.vueを自作した際に<NuxtRouteAnnouncer />を書き忘れている可能性が高いです。

アナウンサーだけでは埋まらない範囲

フォーカスは移動しないのでスキップリンクで補う

読み上げが通るようになっても、フォーカスは押したリンクの位置に残ったままです。Vue RouterもNuxtもフォーカスを動かさないため、キーボード利用者は遷移のたびにヘッダーを最初からタブ移動して本文へ戻ることになります。公式ガイドはアプリ先頭のスキップリンクを定石として挙げ、さらに踏み込む場合のプラグイン例も示しています。

// app/plugins/focus-main.client.ts
export default defineNuxtPlugin(() => {
  useRouter().afterEach((to, from) => {
    if (to.path === from.path) {
      return
    }
    nextTick(() => document.getElementById('main')?.focus())
  })
})

移動先の<main>は元々フォーカスを受け取れないので、tabindex="-1"を付けておく必要があります。正の値を使うとタブ順序そのものが変わるため避けます。

@nuxt/a11yは2026年7月時点でalpha

開発中に問題を見つける手段としては、Nuxt公式の@nuxt/a11yモジュールがあります。ただし最新は1.0.0-alpha.1(2026年1月7日公開)で、公式ドキュメントもalpha段階でAPIが変わる前提と注意しています。本番の品質保証をこれ1つに委ねる段階ではありません。コンポーネント側でアクセシビリティを担保する選択肢としては、Headless UIとは?React/Vueでの使い方・Tailwind連携・主要ライブラリ比較で扱っているヘッドレスUIライブラリの併用も現実的です。

よくある質問

NuxtRouteAnnouncerを使うのに追加インストールは必要ですか?

不要です。Nuxt 3.12.0以降の本体に同梱されており、自動インポートの対象なのでimport文も要りません。モジュールを追加する手順を案内している情報は、Nuxt 3.12より前の時代の外部モジュールと混同している可能性があります。

Nuxt 2やNuxt 3.11以前でも使えますか?

使えません。公式ドキュメントの記載は「Nuxt v3.12+」です。Nuxt 2系(最終版は2.18.1)のレイアウトで使われる<Nuxt />と組み合わせるコード例は成立しません。Nuxt 3以降は<NuxtPage /><NuxtLayout />を使います。

app.vueとlayouts配下のどちらに置くべきですか?

公式ドキュメントはどちらでもよいとしています。全ページで同じ挙動にするならapp.vueが単純です。レイアウトによって読み上げの緊急度を変えたい場合だけ、layouts側に置いてpolitenessを切り替えます。両方に置くとライブリージョンが2つ描画され、同じ文言が二重に通知されうるため、重複配置は避けてください。

読み上げ内容を自分で決めたいときはどうしますか?

遷移に紐づく内容ならuseHeadでtitleを組み立てるのが確実です。遷移と関係ない通知なら、Nuxt 4.4.2以降のuseAnnouncerを使います。useRouteAnnouncerset()は、次のDOM描画でtitleへ戻される点に注意してください。

NuxtRouteAnnouncerを入れればアクセシビリティ対応は終わりますか?

終わりません。埋まるのは「クライアント遷移が支援技術へ通知されない」という穴だけです。フォーカス管理、コントラスト、フォームのラベル付け、キーボード操作は別途対応が必要で、これらはNuxt固有ではなく一般的なWeb実装の課題です。

関連記事

資料請求

RELATED POSTS 関連記事