Next.js の App Router でモバイル表示のスケールを整えたり、スマホのブラウザバーの色(テーマカラー)を変えたりしたいとき、以前は metadata に viewport や themeColor を書いていました。しかし Next.js 14 からはこれらが metadata から分離され、viewport オブジェクトまたは generateViewport 関数として個別に export する形に変わっています。この記事では、Viewport 型を使った静的・動的な設定の書き方、themeColor をライト/ダークで出し分ける方法、各フィールドの意味、そして古い書き方で警告が出る理由までを、動くコードとあわせて解説します。
目次
viewport 設定はなぜ metadata から分離されたのか
ビューポートとは、ブラウザがページを表示する表示領域のことです。スマートフォンで拡大・縮小の初期倍率をそろえたり、端末の幅に合わせてレイアウトさせたりするために、HTML の <head> に <meta name="viewport"> を出力します。テーマカラーも同じく <meta name="theme-color"> として出力され、モバイルブラウザのアドレスバーの色を変えます。
これらは以前 metadata オブジェクトの一部として書けましたが、Next.js 14 で viewport 関連が metadata から独立しました。理由は、ビューポートやテーマカラーがページ本文の SEO 的なメタ情報とは性質が異なり、レンダリングのタイミングや扱いを分けたほうが都合が良いためです。現在は layout.tsx または page.tsx から、viewport という名前のオブジェクト(静的)か、generateViewport という関数(動的)を export して設定します。型は next からインポートする Viewport を使います。
静的な viewport export の基本
もっとも基本的な使い方は、layout.tsx から viewport という定数を export する方法です。値が固定でよい(ページによって変えない)場合はこの静的な形を使います。Viewport 型を付けておくと、フィールド名や値の候補が補完され、タイプミスにも気づけます。
import type { Viewport } from 'next';
// 静的なビューポート設定を export する
export const viewport: Viewport = {
width: 'device-width', // 端末の幅に合わせる
initialScale: 1, // 初期表示の倍率は等倍
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body>{children}</body>
</html>
);
}
この viewport を export すると、Next.js が自動的に <meta name="viewport" content="width=device-width, initial-scale=1"> を生成して <head> に差し込みます。width: 'device-width' は端末の画面幅を基準にする指定で、レスポンシブデザインでは事実上の必須設定です。initialScale: 1 はページを開いたときの拡大率を等倍にします。自分で <meta> タグを書く必要はなく、このオブジェクトを export するだけで済みます。
themeColor と colorScheme を設定する
themeColor は、モバイルブラウザのアドレスバーやツールバーの背景色を指定するフィールドです。単純に1色だけ指定することもできますが、media を使うと prefers-color-scheme(利用者の OS がライトモードかダークモードか)に応じて色を出し分けられます。ライトとダークで見え方を合わせたいときに便利です。
import type { Viewport } from 'next';
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
// ライト/ダークでブラウザバーの色を出し分ける
themeColor: [
{ media: '(prefers-color-scheme: light)', color: '#ffffff' },
{ media: '(prefers-color-scheme: dark)', color: '#0a0a0a' },
],
colorScheme: 'light dark', // 対応する配色スキームを伝える
};
この設定では、ライトモードの端末には content="#ffffff"、ダークモードの端末には content="#0a0a0a" の theme-color メタタグがそれぞれ media 属性付きで出力されます。colorScheme は、ページが対応している配色スキームをブラウザに伝えるフィールドで、'light dark' のように書くと両方に対応していることを示せます。フォームの入力欄やスクロールバーなど、ブラウザ標準の UI の配色がこの値を参考に調整されます。テーマカラーを1色だけでよい場合は themeColor: '#ffffff' のように文字列を直接渡すこともできます。
generateViewport で動的に設定する
ビューポートの内容をルートのパラメータやリクエストの状況に応じて変えたいときは、静的な viewport の代わりに generateViewport 関数を export します。この関数は Viewport を返し、async にして非同期にデータを取得してから値を組み立てることもできます。generateMetadata と同じく、引数から params や searchParams を受け取れます。
import type { Viewport } from 'next';
type Props = {
params: Promise<{ theme: string }>;
};
// ルートパラメータに応じてテーマカラーを出し分ける
export async function generateViewport(
{ params }: Props,
): Promise<Viewport> {
const { theme } = await params;
return {
width: 'device-width',
initialScale: 1,
themeColor: theme === 'dark' ? '#0a0a0a' : '#ffffff',
};
}
export default function Page() {
return <main>テーマ別ページ</main>;
}
この例では、URL の [theme] セグメントが dark のときだけテーマカラーを暗い色にしています。静的な viewport と generateViewport は同時に使えません。値が常に同じなら viewport、パラメータや取得したデータで出し分けたいなら generateViewport、というように用途で使い分けます。多くのサイトは全ページ共通のビューポートで足りるため、まずはルートの layout.tsx に静的な viewport を1つ置く形で十分です。
Viewport 型のフィールド一覧
Viewport 型で指定できる主なフィールドを整理します。width や initialScale などは最終的に <meta name="viewport"> の content 属性にまとめて出力され、themeColor と colorScheme はそれぞれ別のメタタグとして出力されます。
| フィールド | 意味 | 例 |
|---|---|---|
width | ビューポートの幅。端末の幅に合わせるのが基本 | 'device-width' |
initialScale | ページ表示時の初期倍率 | 1 |
maximumScale | 拡大できる最大倍率の上限 | 5 |
userScalable | 利用者による拡大縮小を許可するか | true |
themeColor | ブラウザ UI のテーマカラー。media 付き配列も可 | '#ffffff' |
colorScheme | ページが対応する配色スキーム | 'light dark' |
なお maximumScale を小さくしたり userScalable を false にしたりすると、利用者がページを拡大できなくなります。視力に配慮したアクセシビリティの観点からは、これらでズームを禁止するのは避けたほうがよいとされています。特別な理由がなければ、拡大縮小はブラウザの既定に任せるのが無難です。
metadata に viewport を書くと警告が出るとき
Next.js のバージョンを上げたあと、コンソールに viewport 関連の警告が出ることがあります。これは古い書き方が非推奨になったことが原因です。移行のポイントを2つに分けて説明します。
metadata に書いていた viewport / themeColor は移動する
以前は export const metadata = { viewport: '...', themeColor: '...' } のように metadata の中にビューポートやテーマカラーを書けました。この書き方は非推奨(deprecated)になり、そのまま残しておくと「Unsupported metadata viewport is configured in metadata export」といった警告がビルドや開発サーバーのログに表示されます。警告を消すには、該当するフィールドを metadata から取り出して、別途 viewport オブジェクトとして export し直します。
import type { Metadata, Viewport } from 'next';
// 非推奨: metadata の中に viewport / themeColor を書く形
// export const metadata: Metadata = {
// title: 'My Site',
// themeColor: '#ffffff', // 警告が出る
// viewport: 'width=device-width', // 警告が出る
// };
// 新しい書き方: metadata と viewport を分けて export する
export const metadata: Metadata = {
title: 'My Site',
};
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
themeColor: '#ffffff',
};
このように、SEO 向けの情報(title や description など)は metadata に残し、ビューポートとテーマカラーは viewport に移すのが現在の正しい形です。移行しても出力される <meta> タグ自体は同じなので、表示結果は変わりません。
Client Component では export できない
viewport や generateViewport は、metadata と同じくサーバーコンポーネントからのみ export できます。ファイル先頭に 'use client' が付いた Client Component で export しても機能しません。設定したのにメタタグが出力されないときは、そのファイルがクライアントコンポーネントになっていないかをまず確認します。また viewport の export は layout.tsx か page.tsx に置く必要があり、通常のコンポーネントファイルに書いても読み取られない点にも注意してください。
まとめ
Next.js 14 以降、ビューポートとテーマカラーは metadata から分離され、layout.tsx や page.tsx から viewport オブジェクト(静的)または generateViewport 関数(動的)を export して設定します。型は next からインポートする Viewport を使い、width: 'device-width' と initialScale: 1 がレスポンシブの基本です。themeColor は media で prefers-color-scheme に応じてライト/ダークの色を出し分けられ、colorScheme でページが対応する配色スキームを伝えられます。パラメータやデータで出し分けたいときだけ generateViewport を使い、そうでなければ静的な viewport で十分です。古い metadata 内の書き方は非推奨で警告が出るため、新しい viewport export へ移行しておきましょう。