NuxtRouteAnnouncerは、クライアントサイドのルート遷移が起きたことをスクリーンリーダーへ伝える、Nuxt組み込みのコンポーネントです。Nuxt 3.12.0(2024年6月11日公開)から本体に同梱されていて、追加パッケージは要りません。この記事ではNuxt 4.6.3(2026年10月27日公開)時点の公式ドキュメントと本体ソースをもとに、置き場所、politenessとatomicが出力するHTML、読み上げが起きないときの原因、Nuxt 4.4.2で加わったNuxtAnnouncerとの役割分担、そしてNext.jsのnext-route-announcerとの違いを整理します。
まとめ
- NuxtRouteAnnouncerはNuxt 3.12.0以降のコアに同梱。公式スターターの最小テンプレートではapp.vueに最初から書かれているが、Nuxt UIテンプレートには入っていない。
- 読み上げる文言はページの
<title>だけ。Next.jsのようにh1やURLへ代替しないので、ルート間でtitleが同じか未設定なら何も読まれない。 politenessは出力されるroleとaria-liveを切り替える(polite→role="status"、assertive→role="alert"、off→role属性なし)。atomicの既定値はfalse。- フォーム検証やトーストなど遷移を伴わない読み上げは
NuxtAnnouncerとuseAnnouncerの担当(Nuxt 4.4.2以降)。NuxtRouteAnnouncerで代用すると、Unheadがheadを描画し直したときに文言がtitleへ戻る。 - Next.jsは遷移の読み上げを既定で組み込み、
assertiveで読ませる。Nuxtは自分で置く方式で、既定はpolite。 - アナウンサーを置いてもフォーカスは移動しない。スキップリンクなどの手当ては別に要る。
NuxtRouteAnnouncerが埋める「ページが変わったことが伝わらない」問題
ハイドレーション後のクライアント遷移と通知の欠落
ブラウザがドキュメントを読み込み直したとき、スクリーンリーダーは新しいページの読み込みを自力で検知して読み上げます。ところがNuxtとは:Vue.jsとの違い・レンダリング方式・4系への移行手順と採用判断を実装視点で解説で扱っているとおり、Nuxtアプリはハイドレーションが済むとクライアント側でルーティングします。ドキュメントは再読み込みされないので、ブラウザは新しいページの通知もフォーカスのリセットもしません。リンクを押した利用者には、画面が差し替わった手がかりが何も届かない状態になります。
この穴を埋めるのがNuxtRouteAnnouncerです。Nuxt公式のアクセシビリティガイドは、隠しライブリージョンを描画し、遷移のたびに新しいページタイトルをそこへ書き込む仕組みだと説明しています。SPA・SSR・SSGでこの前提がどう変わるかはレンダリングとは?ブラウザ描画の仕組みとCSR/SSR/SSGの違い・選定基準を解説で整理しています。
実際に出力されるDOMとaria属性
コンポーネントは次の構造を1つ描画します。外側のspan.nuxt-route-announcerをposition: absoluteで浮かせ、内側のspanをクリップして見えなくしたうえで、支援技術からは読める状態に保つ形です。内側のstyleは本体ソースのnuxt-route-announcer.tsにある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-live・aria-atomic・roleの意味はWAI-ARIAとは?role・aria属性の使い方とネイティブHTMLを優先すべき理由で扱っています。
NuxtRouteAnnouncerの設置場所とtitle設定
app.vue・layouts配下への1か所配置
導入はコンポーネントを1つ書くだけで、パッケージの追加もnuxt.configへの追記も要りません。公式スターター(nuxt/starter)の最小テンプレートは、3系用(v3ブランチ・2024年6月11日に追加)と4系用(main・v4ブランチ)のどちらもapp.vueに<NuxtRouteAnnouncer />を書いています。app.vueを自作したプロジェクトでは、公式ドキュメントの例どおりNuxtLayoutの前に追記します。
<!-- app/app.vue -->
<template>
<NuxtRouteAnnouncer />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
Nuxt 4ではソースディレクトリがapp/配下へ移ったため、パスはapp/app.vueとapp/layouts/になります。ディレクトリ構成の変更点はNuxt 4とは?最新バージョンは4.6|確認方法・Nuxt 3との違い・導入手順【2026年10月】にまとめました。レイアウトごとに置き分ける必要はなく、アプリ全体で1か所に置けば足ります。アナウンサーの実体はNuxtアプリ単位で1つだけ作られ、共有されるためです。
Nuxt UIのUAppと併用するときの置き場所
Nuxt UIで始めたプロジェクトは注意が要ります。nuxt/starterのNuxt UI用テンプレート(uiブランチ)のapp.vueは<UApp><NuxtPage /></UApp>だけで、NuxtRouteAnnouncerが入っていません。UAppのソースはreka-uiのConfigProvider・TooltipProviderに加えてトーストやオーバーレイ、ロケールを受け持ちますが、ルート遷移の読み上げ要素は持っていません。最小テンプレートから始めた場合と違い、自分で足さない限り遷移は通知されないということです。
<!-- app/app.vue(Nuxt UI) -->
<template>
<UApp>
<NuxtRouteAnnouncer />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</UApp>
</template>
NuxtRouteAnnouncerはUAppの外に置いても動きます。それでも中に置くのは、テンプレートのルート要素をUApp1つに保っておけば、あとからNuxtAnnouncerを足すときも置き場所で迷わないからです。
ページごとに異なる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ならalert、offなら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)、コンポーネントが内部で使うアナウンサーの実体を取り出します。返り値はmessage・politenessの2つのRefと、set(message, politeness)・polite(message)・assertive(message)の3メソッドです。内部型には後始末用の_cleanupもありますが、戻り型がOmit<RouteAnnouncer, '_cleanup'>なので公開された型には含まれません(実行時の返却オブジェクトには残る内部APIです)。公式ガイドは検索結果の件数通知を例にしています。次の例は初回の取得が終わってからwatchを登録するので、初回の件数は通知せず、その後resultsが変わったときだけ通知します。APIが配列を返す前提です。
<!-- 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>
Unheadのhead反映による通知文言の上書き
実装で最もつまずきやすいのがここです。route-announcer.tsのアナウンサーは生成時にUnheadのdom:renderedフックを登録し、Unheadがtitleやmetaなどheadの内容をDOMへ反映し終えるたびにdocument.titleを読み直してメッセージを上書きします。
function _updateMessageWithPageHeading () {
set(document?.title?.trim(), politeness.value)
}
activeHead?.hooks?.hook('dom:rendered', _updateMessageWithPageHeading)
set()で入れた独自の文言は、次にUnheadがheadを反映した時点でtitleへ戻ります。きっかけになるのはページ遷移、useHeadに渡した値の変化、useHeadを持つコンポーネントの表示と破棄など、headに変更が入る操作です。本文だけのリアクティブ更新ではこのフックは発火しません。headを頻繁に書き換える画面ほど、書いたはずの文言が読まれない現象に当たります。独自の文言を確実に読ませたいなら、set()ではなくuseHeadのtitleを書き換えるか、次章のuseAnnouncerへ移します。このフックが読むのはdocument.titleだけで、h1やURLを代わりに使う処理はありません。
NuxtAnnouncer(Nuxt 4.4.2以降)との使い分け
遷移の通知と、遷移を伴わない通知は別コンポーネント
Nuxt 4.4.2(2026年3月12日公開)で<NuxtAnnouncer>とuseAnnouncerが加わり、フォーム検証・トースト・読み込み状態といった遷移を伴わない読み上げの受け皿ができました。NuxtAnnouncerの公式ドキュメントも、この用途では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以降では選ぶ理由がありません。前章のとおりUnheadのhead反映と同時にdom:renderedでtitleへ戻されるため、通知とheadの変更が重なると独自の文言が消えます。useAnnouncer側のアナウンサーはtitleを監視せず、_cleanup()も文言を空に戻すだけなので、指定した文言がそのまま残ります。
Nuxt 3系(最新は2026年8月公開の3.21.11)にはNuxtAnnouncerがありません。3.21.11のリリースノートは3系がサポート終了(end-of-life)になったと告知しているので、以下は4系へ移るまでのつなぎとして考えます。3系に留まる間はuseRouteAnnouncerのset()に頼らず、通知したい内容を含めてtitleを書き換えるか、自前でaria-live領域を持つコンポーネントを作るほうが挙動が安定します。公式ドキュメントも、このコンポーネントは省略可能で、ソースを土台に自作してよいとしています。
Next.jsのnext-route-announcerとの違い
Next.jsの自動組み込みとNuxtの手動配置
Next.jsから移ってきた人が戸惑うのが、この組み込み方の差です。Next.jsの公式ドキュメントは、next/linkによるクライアント遷移を支援技術へ伝えるためにroute announcerを既定で含めると書いています。App Routerでは、ルーター本体がAppRouterAnnouncerを常に描画し、<next-route-announcer>というカスタム要素をbody末尾に足して、そのshadow DOMの中に読み上げ用のdivを置きます(Next.js 16.4.3のapp-router-announcer.tsx)。開発者ツールで見かけるnext-route-announcerはこの要素で、開発者が何かを書いた結果ではありません。
NuxtのNuxtRouteAnnouncerは、app.vueかlayoutsに書かない限り描画されません。最小テンプレートに最初から入っているので組み込みのように見えますが、テンプレートを差し替えた時点で消えます。
| 観点 | Nuxt(NuxtRouteAnnouncer) | Next.js(App Router) |
|---|---|---|
| 組み込み方 | app.vue等に手動で置く | ルーターが自動で描画 |
| 出力要素 | span.nuxt-route-announcer | next-route-announcer(shadow DOM) |
| 既定の緊急度 | polite(role=”status”) | assertive(role=”alert”) |
| 緊急度の変更 | politeness propで可 | 設定項目なし |
| 読む文言 | document.titleのみ | document.title、無ければh1 |
| 文言の差し替え | slot・useRouteAnnouncer | なし(titleを変える) |
title未設定時のフォールバックの違い
移植で問題になるのは文言の決め方です。Next.jsはtitleが空なら最初の<h1>を読みます。ドキュメントはさらにURLのパスへ代替すると書いていますが、その処理があるのはPages Routerのroute-announcer.tsxで、App Routerの実装はh1までです。Nuxtはtitleしか見ません。Next.jsでtitleを付けず、h1頼みで読み上げが成立していた画面をNuxtへ移すと、遷移しても無音になります。移植するときは、全ページにuseHeadでtitleを付けるところから始めます。
緊急度も逆です。Next.jsはtitleが変わる遷移のたびにassertiveで即時の通知を求め、Nuxtは既定のpoliteで読み上げの切れ目での通知を求めます。実際のタイミングは支援技術と利用者の設定で変わります。Next.jsと同じ聞こえ方に揃えたいならpoliteness="assertive"を指定できますが、前章の理由から既定のpoliteを勧めます。React側の構成はNext.jsとは?16系App Routerで最小アプリを動かす手順と採用判断で扱っています。
読み上げが起きないときの切り分け手順
MutationObserverによるDOM更新の確認
まずは音ではなく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,
})
}
遷移してもログが出ないなら、titleが変わっているかと、監視している要素が描画し直されていないかを確認します。ログが出るのに読み上げられないときは、まずaria-liveの値と、祖先要素にdisplay: noneやaria-hiddenが付いていないかを見て、それでも原因が無ければブラウザとスクリーンリーダーの組み合わせを調べます。実機で確かめる段階では、NVDA・JAWS・VoiceOverで読み上げエンジンごとにライブリージョンの扱いが違うため、対象利用者の環境に合わせて選びます。
読み上げられない4つの原因
- コンポーネントが描画されていない:
.nuxt-route-announcerが見つからない状態です。app.vueを自作した、またはNuxt UIテンプレートから始めたときに起きます。 - ルート間でtitleが同じ、または未設定:ライブリージョンの文字列が変わらないので通知は起きません。
useHeadでページごとのtitleを設定します。 politeness="off"のまま:aria-live="off"が出力され、role属性も付きません。通常の遷移ではこの領域にフォーカスが無いため、更新は通知されないのが仕様どおりの挙動です。set()の文言が上書きされている:dom:renderedフックがtitleを入れ直します。独自の文言はtitle側かuseAnnouncerへ移します。
初回のフルページロードでアナウンサーが鳴らないのは正常な動作です。ライブリージョンは初期表示の内容ではなく変化を通知する仕組みで、通常のページ読み込みはスクリーンリーダー自身が読み上げます。
アナウンサーだけでは埋まらない範囲
スキップリンクとフォーカス移動による本文への誘導
読み上げが通るようになっても、フォーカスは押したリンクの位置に残ったままです。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年11月時点でもalpha
開発中に問題を見つける手段には、Nuxt公式の@nuxt/a11yモジュールがあります。ただしnpmの最新は2026年11月1日時点でも2026年1月に公開された1.0.0-alpha.1のままで、公式ガイドもalpha段階でAPIが変わる前提だと注意しています。本番の品質保証をこれ1つに任せる段階ではありません。コンポーネント側でアクセシビリティを担保するなら、Headless UIとは?React・Vue版の違いとTailwind連携・Next.jsでの使い方【v2.2対応】で扱っているヘッドレスUIライブラリを併用する方法もあります。遷移の読み上げ、フォーカス、コントラスト、フォームのラベルまでをWCAGの達成基準に沿って点検する体制が社内に無い場合は、Webアクセシビリティ対応の支援を使う手もあります。
よくある質問
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つ描画され、同じ文言が二重に通知されうるので、重複配置は避けてください。
next-route-announcerのように自動で組み込まれませんか?
組み込まれません。next-route-announcerはNext.jsのルーターが自動で足す要素で、Nuxtではapp.vueかlayoutsに<NuxtRouteAnnouncer />を書いたときだけ描画されます。最小テンプレートには最初から書かれていますが、Nuxt UIテンプレートには無いので、自分で追記します。
NuxtRouteAnnouncerを入れればアクセシビリティ対応は終わりますか?
終わりません。埋まるのは、クライアント遷移が支援技術へ通知されないという穴だけです。フォーカス管理、コントラスト、フォームのラベル付け、キーボード操作は別に対応が要り、これらはNuxt固有ではなくWeb実装全般の課題です。