1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js App Router で Emotion と外部スタイルシートの CSS 適用順を制御する

1
Posted at

はじめに

Next.js App Router に Emotion を導入して外部スタイルシートと組み合わせると、CSS の適用順が意図通りにならないケースがあります。

私が実際にハマったのは、通常のページでは問題なく表示されているのに、エラーページのみアイコンのサイズがおかしくなるという状況でした。原因は <head> 内のスタイル関連タグの挿入位置を制御していなかったことで、エラーページでは通常ページと異なる位置にタグが挿入され、CSS の適用順が逆転していたことでした。

この記事では以下の内容を扱います。

  • App Router で Emotion を動かすための基本セットアップ
  • なぜエラーページでだけ問題が起きたのか
  • useServerInsertedHTML で挿入順を制御する解決策

対象読者: Next.js App Router + Emotion を使っていて、外部 CSS とのスタイル衝突に悩んでいる方

Next.js App Router で Emotion を使うための基本設定

App Router で Emotion を使うには、通常の Next.js プロジェクトに比べてひと手間必要です。これは Emotion が App Router に正式対応していないことが背景にあります。Next.js の公式ドキュメント(CSS-in-JS)でも、Emotion は「currently working on support」と記載されており、対応作業が進行中です。

tsconfig.json

jsxImportSource@emotion/react を指定することで、JSX が自動的に Emotion の jsx 関数を通るようになります。

tsconfig.json(一部抜粋)
{
  "compilerOptions": {
    "jsxImportSource": "@emotion/react"
  }
}

Server Component での注意点

App Router ではコンポーネントがデフォルトで Server Component になります。jsxImportSource: "@emotion/react" を設定すると全 JSX が Emotion の jsx 関数を通るようになりますが、Server Component 環境ではこれが動作しません。そのため、Server Component のファイル先頭に以下のプラグマを書いて React のデフォルトに戻します。

/* @jsxImportSource react */

Client Component では 'use client' を付ければ Emotion がそのまま使えます。

詳しくはこちらの記事が参考になりました。

発生した問題: エラーページでのアイコンサイズ崩れ

私が開発していたページでは、アイコンに Material Symbols(Google が提供する外部フォント CSS)を使っていました。Material Symbols は next/font/google に対応していないため、layout.tsx<head><link> タグを直接書いていました。

<head>
  <link
    rel="stylesheet"
    href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:..."
  />
</head>

この状態でアイコンのフォントサイズを Emotion で指定したところ、ページによって効いたり効かなかったりする現象が発生しました。

  • 通常のページ → 正しく表示される
  • エラーページ → アイコンのサイズだけがおかしい(他の Emotion スタイルは正常)

原因を紐解く

<head> 内のタグ順序がページによって違う

まず、エラーページと記事ページの <head> の中身を比べてみました。記事ページでは Material Symbols の <link> が Emotion の <style> より前にあるのに対し、エラーページでは逆順になっていました。

<!-- 記事ページ(CSR後)-->
<head>
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+...">
  <style data-emotion="css ...">/* Emotion のスタイル */</style>
</head>

<!-- エラーページ(CSR後)-->
<head>
  <style data-emotion="css ...">/* Emotion のスタイル */</style>
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+...">
</head>

CSS の同一詳細度では後に定義された方が優先されます。エラーページでは Material Symbols の <link> が Emotion の <style> より後に来るため、.material-symbols-outlinedfont-size に関して Material Symbols 側が勝ってしまっていました。

他の Emotion スタイルが正常に動いていたのは、Material Symbols CSS が .material-symbols-outlined などのアイコン専用セレクターしか持たないため、他の Emotion スタイルとは競合しなかったからです。

エラーページでは layout.tsx の挙動が違う

なぜ順序が逆になるのかを調べると、エラーページの SSR 時点での HTML に違いがありました。

not-found.tsx などのエラーページでは、おそらく Next.js の内部的な処理の違いにより、layout.tsx に書いた静的 <head> の JSX が SSR 時点での HTML に含まれないようです。実際に 404 ページの HTML を確認すると、layout.tsx<head> に書いていた <link> タグが一切出力されていませんでした。(代わりに、これらのリソースは React の Flight Protocol を通じて <link><head> に挿入されているようです。)

一方、Emotion はデフォルトではハイドレーション時に <style> タグを <head> に挿入します。この挿入が Flight Protocol より前に行われるため、エラーページでは Emotion → Material Symbols という順序になってしまっていました。

  • Emotion CSS: ハイドレーション時に <style><head> へ挿入(先)
  • Material Symbols CSS: Flight Protocol を通じて <link><head> へ挿入(後)

<head> への挿入を明示的に制御する必要がある

記事ページでは静的 <head> JSX に Material Symbols <link> が含まれるため、SSR 時点での HTML の時点で Emotion より前に存在します。しかしエラーページではその保証がなく、挿入順序が変わってしまいます。

ページ種別に関わらず CSS の適用順を安定させるには、<head> へのタグ挿入を明示的に制御する必要があります。

解決: useServerInsertedHTML で挿入順を明示制御

Material Symbols の <link>useServerInsertedHTML 経由で挿入することで、<head> への挿入順序を制御できます。

外部スタイルシートの挿入コンポーネント

useRef で二重挿入を防ぎながら <link> を挿入します。

'use client'

import { useRef } from 'react'
import { useServerInsertedHTML } from 'next/navigation'

export function MaterialSymbolsStylesheet() {
  const isInserted = useRef(false)

  useServerInsertedHTML(() => {
    if (isInserted.current) return null
    isInserted.current = true
    return (
      <link
        rel="stylesheet"
        href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:..."
      />
    )
  })
  return null
}

useServerInsertedHTML は Next.js が SSR 時に HTML を生成する際に </head> の直前にコンテンツを挿入するフックです。これにより静的 <head> JSX を使わなくても、エラーページを含む全ページで確実に <head> に出力されます。

EmotionRegistry コンポーネント

Next.js の公式ドキュメント(CSS-in-JS)では、styled-components や styled-jsx での useServerInsertedHTML の使い方が紹介されています。Emotion も同じアプローチで対応できます。

Streaming に対応するには cache.insert をモンキーパッチして差分のみを追跡する必要がありますが、ここでは概念を示す簡略版を載せます。詳細な実装は Next.js 公式ドキュメントの CSS-in-JS を参考に、AIエージェントによしなに実装させるとよいと思います。

'use client'

import createCache from '@emotion/cache'
import { CacheProvider } from '@emotion/react'
import { useServerInsertedHTML } from 'next/navigation'
import { useState } from 'react'

export function EmotionRegistry({ children }: { children: React.ReactNode }) {
  const [cache] = useState(() => {
    const cache = createCache({ key: 'css' })
    cache.compat = true
    return cache
  })

  useServerInsertedHTML(() => (
    <style
      data-emotion={`${cache.key} ${Object.keys(cache.inserted).join(' ')}`}
      dangerouslySetInnerHTML={{
        __html: Object.values(cache.inserted).join(' '),
      }}
    />
  ))

  return <CacheProvider value={cache}>{children}</CacheProvider>
}

layout.tsx での配置順

ポイントは MaterialSymbolsStylesheetEmotionRegistry よりに配置することです。useServerInsertedHTML の呼び出し順がそのまま </head> への挿入順になります。

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {/* 先に挿入 = CSS 上で先に定義される */}
        <MaterialSymbolsStylesheet />
        {/* 後に挿入 = CSS 上で後に定義される = こちらが優先 */}
        <EmotionRegistry>
          {children}
        </EmotionRegistry>
      </body>
    </html>
  )
}

これにより、SSR 時に HTML の <head> 内に「Material Symbols → Emotion」の順で CSS が挿入されます。

<head>
  <!-- useServerInsertedHTML で挿入(Material Symbols が先) -->
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+...">
  <!-- useServerInsertedHTML で挿入(Emotion が後 = 優先) -->
  <style data-emotion="css ...">/* Emotion のスタイル */</style>
</head>

おわりに

今回の内容を簡単にまとめます。

  • App Router のエラーページでは layout.tsx の静的 <head> JSX が SSR 時点での HTML に出力されず、スタイル関連タグが通常ページと異なる位置に挿入されることがある
  • <head> 内のタグ挿入位置を制御していないと、ページ種別によって CSS の適用順が変わり、意図しないスタイルが適用されることがある
  • useServerInsertedHTML を使うことで全ページ共通で <head> への挿入を確実に行え、挿入順も制御できる

同じ構成で困っている方の参考になれば幸いです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?