Next.js でスタイルを当てようとしたとき、「CSS ファイルはどこに置けばいいのか」「なぜ globals.css だけ layout.tsx から読み込んでいるのか」と迷うことがあります。Next.js には CSS Modules とグローバル CSS という2つの仕組みが最初から組み込まれていて、ファイル名の付け方と import する場所によって扱いが変わります。この記事では、その規約と使い分け、Sass を使う場合の追加設定までを順番に解説します。
目次
Next.js に用意されているスタイルの当て方
Next.js はビルドの仕組みに CSS の処理が組み込まれているため、追加のローダー設定を書かなくても CSS ファイルを JavaScript / TypeScript から import できます。用意されている方法は大きく次のとおりです。どれか1つに絞る必要はなく、実際のプロジェクトでは組み合わせて使います。
| 方法 | ファイル名 | 適用範囲 |
|---|---|---|
| CSS Modules | *.module.css / *.module.scss | import したコンポーネントの中だけ(クラス名が変換される) |
| グローバル CSS | globals.css など任意の .css | 読み込んだページ以下すべて。書いたセレクタがそのまま効く |
| 外部パッケージの CSS | node_modules 内の .css | グローバル CSS と同じ扱い |
| Sass | *.scss / *.sass | sass をインストールすると上記2つと同様に使える |
| Tailwind CSS | ユーティリティクラス | クラス名を JSX に直接書く。別途セットアップが必要 |
このうち Next.js の「ファイル規約」として押さえておきたいのが、ファイル名に .module. が入っているかどうかで挙動が変わるという点です。Button.module.css は CSS Modules として、styles.css はグローバル CSS として処理されます。同じ内容の CSS でも、名前が違うだけで結果がまったく変わります。
CSS Modules は .module.css を作って import する
CSS Modules は、クラス名をビルド時に一意な名前へ変換することで、コンポーネントごとにスタイルを閉じ込める仕組みです。Next.js では標準で対応しているので、拡張子を .module.css にしたファイルを作るだけで使えます。置き場所の制限もなく、使うコンポーネントの隣に並べて置けます。
.button {
padding: 8px 20px;
border: none;
border-radius: 4px;
background-color: #007bff;
color: #fff;
cursor: pointer;
}
.button:hover {
background-color: #0062cc;
}
コンポーネント側では、このファイルを import styles from './Button.module.css' のようにデフォルトインポートします。受け取った styles は「元のクラス名」をキー、「変換後のクラス名」を値に持つオブジェクトです。className にはそのプロパティを渡します。
import styles from './Button.module.css';
export default function Button({ children }: { children: React.ReactNode }) {
// styles.button に変換後のクラス名が入っている
return <button className={styles.button}>{children}</button>;
}
クラス名はビルド時に一意な名前へ変換される
出力された HTML を開発者ツールで見ると、class="button" ではなく、ファイル名と元のクラス名、それにハッシュを組み合わせた文字列になっています。実際の形式は Next.js のバージョンやビルド設定によって変わりますが、おおよそ次のようなイメージです。
<button class="Button_button__hZ2pQ">送信</button>
この変換があるおかげで、別のコンポーネントで同じ .button というクラス名を使っても衝突しません。Card.module.css の .button と Button.module.css の .button は、最終的に別々のクラス名になるからです。.btn-primary-lg のように長い名前を考えたり、BEM のような命名規則でスコープを人力で管理したりする必要がなくなるのが、CSS Modules の一番の利点です。
逆に言えば、変換後のクラス名は自分で決められません。ハッシュ部分を当てにして別の CSS から狙い撃ちする、といった書き方はできないということです。外部から上書きしたい場合は、後述するように className を props で受け取って合成する形にします。
ケバブケースのクラス名はブラケット記法で取り出す
CSS では .card-title のようにハイフンでつなぐ書き方が一般的ですが、JavaScript のプロパティ名としてはハイフンが使えません。styles.card-title と書くと「styles.card から title を引く」という式として解釈されてしまいます。ハイフンを含むクラス名は、次のようにブラケット記法で取り出します。
import styles from './Card.module.css';
export default function Card() {
return (
<div className={styles.card}>
{/* ハイフンを含むクラス名はブラケット記法で取り出す */}
<h2 className={styles['card-title']}>見出し</h2>
</div>
);
}
毎回ブラケット記法を書くのが煩わしいなら、CSS 側のクラス名を最初からキャメルケース(.cardTitle)で書いてしまうのが手軽です。CSS のクラス名にキャメルケースを使っても何の問題もなく、styles.cardTitle とドット記法で書けるようになります。プロジェクト内でどちらかに統一しておくと、書き方で悩まずに済みます。
複数のクラスをまとめて当てる
className に渡すのは最終的にただの文字列なので、複数のクラスを当てたいときはスペース区切りの文字列を組み立てます。テンプレートリテラルを使うのがもっとも素直な方法です。
import styles from './Button.module.css';
export default function Button() {
// 2つのクラスをスペース区切りで連結する
return (
<button className={`${styles.button} ${styles.primary}`}>
送信
</button>
);
}
条件によってクラスを付け外しする場合は、条件が false のときに空文字になるようにします。テンプレートリテラルの中で論理演算子による短絡評価を使うと、条件が false のときにその false が文字列化されて class="button false" のように紛れ込んでしまいます。三項演算子で空文字を明示するほうが確実です。
import styles from './Button.module.css';
type Props = {
variant?: 'primary' | 'secondary';
disabled?: boolean;
className?: string;
};
export default function Button({ variant = 'primary', disabled, className }: Props) {
// 条件に応じて付けるクラスを配列にまとめ、空の要素を除いて連結する
const classNames = [
styles.button,
styles[variant],
disabled ? styles.disabled : '',
className ?? '',
]
.filter(Boolean)
.join(' ');
return (
<button className={classNames} disabled={disabled}>
送信
</button>
);
}
この例のように className を props で受け取って末尾に足しておくと、呼び出し側から余白などを調整できるようになります。CSS Modules のクラス名は外から推測できないので、コンポーネントの見た目を外部から少しだけ変えたいときは、この「className を受け取れるようにしておく」という形が定番です。条件分岐が増えて読みにくくなってきたら、clsx のような小さなライブラリを入れて整理する方法もあります。
:global() と composes で CSS Modules の外とつなぐ
:global() でクラス名の変換を止める
CSS Modules ファイルの中でも、一部のセレクタだけは変換せずそのまま出力したいことがあります。外部ライブラリが付けるクラス名や、dangerouslySetInnerHTML で流し込んだ HTML の中のクラスなど、自分で styles オブジェクト経由では指定できないものが対象です。そういうときは :global() で囲みます。
.article {
line-height: 1.9;
}
/* .article は変換され、.wp-block-table はそのまま出力される */
.article :global(.wp-block-table) {
margin: 32px 0;
}
/* 単独で使うとファイル内のどこにも紐づかない完全なグローバル指定になる */
:global(.no-scroll) {
overflow: hidden;
}
1つ目のように「変換されるクラスの下に :global() を置く」書き方なら、影響範囲がそのコンポーネントの中に限定されるので安全です。2つ目のように単独で書くとサイト全体に効いてしまい、CSS Modules を使う意味が薄れます。どうしても必要なとき以外は避けて、本当に全体で使うものはグローバル CSS 側にまとめるほうが管理しやすくなります。
composes で共通のスタイルを引き継ぐ
同じファイル内、あるいは別のモジュールファイルのクラスを取り込みたいときは composes が使えます。CSS Modules 固有の記法で、指定したクラスの内容をコピーするのではなく「両方のクラスが付いた状態」を作ります。
.base {
padding: 8px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
/* 同じファイル内の .base を取り込む */
.primary {
composes: base;
background-color: #007bff;
color: #fff;
}
/* 別ファイルから取り込む場合は from でパスを書く */
.secondary {
composes: outline from './shared.module.css';
color: #007bff;
}
styles.primary を className に渡すと、実際には base と primary、2つの変換済みクラス名がスペース区切りで入った文字列が返ります。JSX 側で毎回2つ連結して書く必要がなくなるのが利点です。ただし composes はセレクタの一番最初に書く必要があり、要素セレクタや複合セレクタには使えません。使いどころを絞らないと CSS の依存関係が追いにくくなるので、ボタンや見出しのようにバリエーションが決まっているものに限定するのがおすすめです。
グローバル CSS はどこから読み込めるのか
リセット CSS、body のフォント指定、CSS カスタムプロパティ(変数)の定義など、サイト全体に効かせたいスタイルはグローバル CSS に書きます。create-next-app で作ったプロジェクトなら app/globals.css が最初から用意されていて、ルートレイアウトで読み込まれています。
:root {
--color-text: #333;
--color-link: #007bff;
}
body {
margin: 0;
color: var(--color-text);
font-family: system-ui, sans-serif;
line-height: 1.8;
}
// このスタイルはアプリ内のすべてのルートに適用される
import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body>{children}</body>
</html>
);
}
CSS ファイルを import しているのに、そのファイルの中で globals のような変数を使っていない点に違和感があるかもしれません。CSS の import は「値を取り込む」ためではなく、このファイルが使われるときにこの CSS も読み込めとビルドツールに伝えるためのものです。だから import 文だけを書き、返り値は受け取りません。
App Router では app 配下ならどこからでも import できる
App Router の場合、グローバル CSS は app ディレクトリ内のレイアウト・ページ・コンポーネントのどこからでも import できます。node_modules にある外部パッケージのスタイルシート(Bootstrap など)も同様です。制限がないぶん自由ですが、公式ドキュメントは本当に全体で必要なものだけをグローバルにし、ルートで読み込むことを推奨しています。
理由は、Next.js が React 標準のスタイルシート対応に乗っているためで、ルート間を移動したときに読み込み済みのスタイルシートが取り除かれません。特定のページのコンポーネントからグローバル CSS を読み込むと、そのページを離れたあとも指定が残り、別のページの見た目に影響してしまう可能性があります。ページ固有のスタイルは CSS Modules で書く、というのが安全な線引きです。
Pages Router では _app.tsx からしか読み込めない
Pages Router を使っている場合はもっとはっきりした制約があり、グローバル CSS を import できるのは pages/_app.tsx だけです。それ以外のページやコンポーネントから .css を import すると、ビルド時にエラーになります。
Global CSS cannot be imported from files other than your Custom <App>. Please move all first-party global CSS imports to pages/_app.js. Or convert the import to Component-Level CSS (CSS Modules).
メッセージが示しているとおり、対処は2つです。全体に効かせたいスタイルなら pages/_app.tsx の import に移す。そのコンポーネントだけのスタイルなら、ファイル名を .module.css に変えて CSS Modules に切り替える。App Router に移行済みのプロジェクトではこのエラーは出ませんが、古い記事やサンプルを参考にしているとこの制約の話が出てくるので、どちらのルーターの話なのかを意識して読むと混乱しません。
グローバル CSS と CSS Modules の書き分け
迷ったときは「そのスタイルはページを離れても残っていてよいか」で判断すると分かりやすくなります。body の背景色やフォント、CSS 変数の定義、リセット CSS は残っていて構わないのでグローバルへ。ボタンやカード、記事本文のレイアウトのように特定のコンポーネントに紐づくものは CSS Modules へ、という具合です。
グローバル CSS に定義した CSS 変数は、CSS Modules 側からもそのまま var(--color-link) で参照できます。変換されるのはクラス名だけで、カスタムプロパティは対象外だからです。色やフォントサイズの定義をグローバルに集約し、それを各モジュールから参照する形にしておくと、テーマの変更が1か所で済みます。
Sass を使うなら .module.scss にする
ネストや @use といった Sass の機能を使いたい場合は、sass パッケージをインストールするだけで有効になります。Next.js 側の設定ファイルを書き換える必要はありません。
npm install --save-dev sass
インストール後は .scss と .sass の両方が使えるようになります。CSS Modules として使いたい場合は、CSS のときと同じ考え方で .module.scss(インデント構文なら .module.sass)という名前にします。どちらの構文にするか決めかねているなら、素の CSS がそのまま通る .scss から始めるのが無難です。
.card {
padding: 24px;
border: 1px solid #ddd;
/* ネストして書ける。& は親セレクタに展開される */
.title {
margin: 0 0 12px;
font-size: 20px;
}
&:hover {
border-color: #007bff;
}
}
この場合、コンポーネント側からは styles.card と styles.title の両方が取り出せます。ネストして書いてもクラス名の変換は個別に行われるためです。全ファイルで共通の変数を自動で読み込ませたいときなど、Sass の挙動を細かく調整したい場合は next.config.ts の sassOptions を使います。
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
sassOptions: {
// すべての Sass ファイルの先頭に差し込まれる
additionalData: `@use "@/styles/variables" as vars;`,
},
};
export default nextConfig;
Server Components でも Client Components でも同じように使える
App Router では、コンポーネントは既定で Server Component になり、'use client' を書いたものだけが Client Component になります。CSS Modules とグローバル CSS はどちらの場合でもそのまま使えます。クラス名の解決がビルド時に完了していて、ブラウザ上で JavaScript を実行する必要がないためです。
// 'use client' が無いので Server Component。CSS Modules はそのまま使える
import styles from './page.module.css';
export default function Page() {
return (
<main className={styles.main}>
<h1 className={styles.title}>ようこそ</h1>
</main>
);
}
ここが CSS-in-JS との大きな違いです。styled-components や Emotion のように実行時にスタイルを生成するライブラリは、React の状態やコンテキストを前提にしているため Server Components ではそのまま動かず、'use client' を付けたコンポーネントの中で使うことになります。「Server Component のままスタイルを当てたい」という要件なら、CSS Modules かグローバル CSS、あるいは Tailwind CSS が選択肢になります。
Tailwind CSS や CSS-in-JS とどう使い分けるか
Tailwind CSS は、あらかじめ用意されたユーティリティクラスを JSX に直接並べていく方式です。CSS ファイルを作らずに済み、クラス名を考える手間もなくなりますが、マークアップにクラスが増えて長くなります。CSS Modules は逆に、見た目の指定を CSS ファイル側にまとめられるため、複雑なセレクタやアニメーションを書きたい場面で扱いやすくなります。両方を併用して、細かい調整だけ CSS Modules に逃がすという構成もよく取られます。
CSS-in-JS は、props の値をそのままスタイルに反映できるのが強みです。前述のとおり Client Component 側の話になるので、ページ全体をこの方式で組むと、本来サーバー側で完結できたコンポーネントまでクライアント側に寄ってしまいます。props に応じた動的なスタイルが必要な部分は、CSS Modules でクラスを切り替える、あるいは CSS 変数を style 属性で渡してモジュール側の CSS から参照する、という方法でも対応できます。
import styles from './Bar.module.css';
export default function Bar({ percent }: { percent: number }) {
// 動的な値は CSS 変数として渡し、CSS 側で var() で受け取る
return (
<div
className={styles.bar}
style={{ '--bar-width': `${percent}%` } as React.CSSProperties}
/>
);
}
読み込み順に左右されないスタイルの書き方
Next.js は本番ビルドのときに CSS をまとめて分割しますが、その並び順はコード上で import した順に依存します。あるコンポーネントを先に import していると、そのコンポーネントの CSS も先に配置される、という具合です。開発サーバーと本番ビルドで順序が変わることもあるため、順序を前提にしたスタイルは思わぬ形で崩れます。
つまり「あとから読み込まれるはずだから、こちらの指定が勝つ」という前提を置かないことが大事です。同じ要素に対して複数の場所から同じプロパティを指定していると、どちらが適用されるかがビルド結果次第になってしまいます。ある要素の見た目は1か所の CSS Modules ファイルで完結させ、他の場所からは触らない形にしておけば、この問題自体が起きません。
同じ理由で、!important や div.card .title span のような詳細度の高いセレクタで上書きの勝敗をコントロールするのも避けたほうがよい書き方です。CSS Modules ではクラス名が一意になっているので、そもそも意図しない衝突は起きません。単一のクラスセレクタで書き、バリエーションはクラスの付け替えで表現するのが、順序にも詳細度にも依存しない書き方になります。
公式ドキュメントも、CSS の import はできるだけ入り口となるファイルにまとめること、グローバルなスタイルシートはアプリケーションのルートで読み込むこと、そして import 文を自動で並べ替えるリンターやフォーマッタの設定を切っておくことを挙げています。import の並び順が CSS の順序に直結する以上、ツールが勝手に並べ替えると結果が変わってしまうためです。
まとめ
Next.js のスタイル指定は、ファイル名の規約を押さえるとすっきり理解できます。Button.module.css のように .module.css で終わるファイルは CSS Modules として扱われ、import styles from './Button.module.css' で読み込んで className={styles.button} と書きます。クラス名はビルド時に一意な名前へ変換されるので、コンポーネント間で名前が衝突しません。ハイフンを含むクラス名は styles['card-title'] のブラケット記法で取り出し、複数のクラスはテンプレートリテラルや配列の join で連結します。
グローバル CSS は app/globals.css を app/layout.tsx から import するのが基本形です。App Router では app 配下のどこからでも読み込めますが、ルートを移動しても外れないため、リセット CSS や CSS 変数のような本当に全体で必要なものに限るのが安全です。Pages Router では pages/_app.tsx 以外から import するとエラーになるので、コンポーネント固有のスタイルは CSS Modules に切り替えます。Sass を使うなら npm install --save-dev sass のうえで .module.scss にすれば、同じ仕組みがそのまま働きます。まずは CSS Modules を基本に据えて、順序や詳細度に頼らない形で書き進めてみてください。