[Next.js] アクセシビリティとレンダリングエラーの解決方法

[Next.js] アクセシビリティとレンダリングエラーの解決方法

illustrated by soto

Next.jsプロジェクトで発生した複数のエラー

Next.jsとCloudflare Pagesで構築しているサイトで、以下のエラーが発生した。

  1. aria-hiddenとフォーカス可能な要素の競合
  2. allowFullScreenallow属性の競合
  3. React error #418(Suspense境界でのテキストノード問題)
  4. dynamic = "error"による404エラー

それぞれのエラーの原因と解決方法をまとめる。

1. aria-hiddenとフォーカス可能な要素の競合

エラー内容

error

Blocked aria-hidden on an element because its descendant retained focus. The focus must not be hidden from assistive technology users. Avoid using aria-hidden on a focused element or its ancestor. Consider using the inert attribute instead.

原因

ドロワーメニューが閉じている状態でaria-hidden="true"が設定されているにもかかわらず、内部のリンク(<a>タグ)がフォーカス可能な状態になっていた。

// 問題のあるコード
<nav aria-hidden={!open}>
  <Link href="/">top</Link>  {/* メニューが閉じていてもフォーカス可能 */}
</nav>

CSSの transform: translateX(-100%) で画面外に隠しているだけで、DOMには存在しているため、フォーカスが当たってしまう。

解決方法:inert属性を使用

メニューが閉じている時に inert 属性を付与することで、フォーカスを完全にブロックする。

// 修正後
<nav
  aria-label="サイト内メニュー"
  className={`${styles.drawer} ${open ? styles.drawerOpen : ""}`}
  {...(!open && { inert: true })}
>
  <Link href="/">top</Link>
</nav>
/* CSS */
.drawer {
  transform: translateX(-100%);
  transition: transform 0.25s ease;
}
 
.drawerOpen {
  transform: translateX(0);
}

これにより:

  • inert 属性でメニューが閉じている時はフォーカス不可能になる
  • CSSトランジションによるスライドアニメーションが正常に機能
  • aria-hiddeninert に置き換えることで警告を根本的に解決できる

注意: 条件付きレンダリング({open && <nav>...</nav>})を使うとアニメーションが失われるため、要素は常にDOMに存在させる必要がある。

2. allowFullScreenとallow属性の競合

エラー内容

error

Allow attribute will take precedence over 'allowfullscreen'.

原因

Spotify埋め込みiframeで、古いallowFullScreen属性と新しいallow属性の両方が設定されており、allow属性にfullscreenが含まれているため競合していた。

// 問題のあるコード
<iframe
  allowFullScreen
  allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
/>

解決方法:allowFullScreenを削除

現代的なベストプラクティスは allow 属性のみを使用すること。

// 修正後
<iframe
  allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
/>

allow属性にfullscreenが含まれているため、フルスクリーン機能は引き続き有効。

3. React error #418(Suspense境界の問題)

エラー内容

error

Uncaught Error: Minified React error #418

原因

Client ComponentがフラグメントをルートとするとServer/Clientの境界でhydrationミスマッチが起きやすく、Suspense #418エラーに繋がる。今回はフラグメントの直下に <h1> という通常のHTML要素とフォームコンテナが混在していたことが原因だった。

// 問題のあるコード(ContactForm.tsx)
function ContactForm() {
  return (
    <>
      <h1>Contact</h1>
      <div className={styles.contactFormContainer}>
        {/* フォーム内容 */}
      </div>
    </>
  );
}

解決方法:単一ルート要素を返す

Client Componentは単一のルート要素を返すようにし、<h1>はServer Component側で管理する。

// 修正後(page.tsx)
export default function ContactPage() {
  return (
    <main>
      <h1>Contact</h1>
      <ContactForm />
    </main>
  );
}
 
// 修正後(ContactForm.tsx)
function ContactForm() {
  return (
    <div className={styles.contactFormContainer}>
      {/* フォーム内容 */}
    </div>
  );
}

これにより:

  • Server ComponentとClient Componentの境界が明確になる
  • Suspenseエラーが解消される

4. dynamic = "error" による404エラー

エラー内容

error

GET /note/nextjs-cloudflare-edge-runtime?_rsc=icfcq 404 (Not Found) GET /schedule/live-event?_rsc=s5roe 404 (Not Found)

原因

動的ルート([slug]/page.tsx)でexport const dynamic = "error"を設定していた。これは動的レンダリングが発生した場合にエラーを投げる設定。

Next.jsのクライアントサイドナビゲーションでは、RSC(React Server Components)ペイロードを取得するために?_rsc=...クエリパラメータ付きリクエストを送信する。このリクエストが動的リクエストとして扱われ、404エラーが発生していた。

// 問題のあるコード
export const dynamic = "error"; // 動的リクエストでエラー
 
export async function generateStaticParams() {
  const posts = await getAllPostMeta();
  return posts.map(({ slug }) => ({ slug }));
}

解決方法:force-staticに変更

// 修正後
export const dynamic = "force-static";
 
export async function generateStaticParams() {
  const posts = await getAllPostMeta();
  return posts.map(({ slug }) => ({ slug }));
}

これにより:

  • すべてのページがビルド時に静的生成される
  • クライアントサイドナビゲーション時のRSCリクエストも適切に処理される
  • generateStaticParams()で事前に定義されたパスのみが生成される

dynamic設定の違い

Next.jsのdynamic設定には以下の選択肢がある:

設定値 動作
"auto" デフォルト。動的関数が使われた場合のみ動的レンダリング
"force-static" すべて静的生成。動的関数使用時もビルド時の値を使用
"force-dynamic" すべて動的レンダリング(SSR)
"error" 動的レンダリングが発生するとエラー

Cloudflare Pagesのような静的ホスティング環境では、"force-static"が推奨される。

まとめ

  • アクセシビリティエラーは、DOM構造とARIA属性の整合性を保つことで解決
  • 属性の競合は、現代的なWeb標準に従うことで解決
  • Reactエラーは、Server ComponentとClient Componentの責務を明確にすることで解決
  • 動的レンダリングエラーは、静的生成の設定を適切に行うことで解決

Next.js App Router + Cloudflare Pagesの構成では、これらのエラーに遭遇することが多い。

参考リンク