なぜCSS設計は破綻するのか? C層(Component)が`width`を持つことの弊害
Next.jsとCSS Modulesによるコンポーネントベースの開発は非常に強力です。私たちは「再利用可能なコンポーネント」を目指し、src/components/Button/Button.tsx のようなコンポーネントを作成します。そして、Button.module.scss にスタイルを記述します。
しかし、プロジェクトが進行すると、この`Button`コンポーネントが様々な場所で使われ始めます。
- トップページのヒーローセクションでは、幅いっぱいのボタン(
width: 100%)が欲しい。 - サイドバーでは、小さなボタン(
width: 120px)が欲しい。 - 記事カード内では、右下に配置(
margin-left: auto)したい。
この時、もし`C-Button.module.scss`が「width: 200px;」や「margin-bottom: 16px;」といった外側(レイアウト)のスタイルを自身で定義していたら、どうなるでしょうか?
そうです。配置する場所(コンテキスト)ごとに、これらのスタイルを上書きする必要が発生します。これがCSS設計破綻の第一歩です。
!important、`style` prop、コンテキスト依存… 肥大化するCSS
この「上書き」要求は、多くの場合、好ましくない設計パターンにつながります。
!importantの乱用
最も手軽な上書き手段ですが、詳細度(Specificity)の秩序を破壊し、将来的なメンテナンスを不可能にします。
style prop や className prop での上書き
<Button className="sidebar-button" /> のように親からクラス名を渡す方法です。一見良さそうに見えますが、C層のコンポーネントが「親からどんなクラス名で上書きされるか」を意識する必要があり、結合度が高くなります。
コンテキスト依存のprop
<Button isFullWidth /> や <Button context="sidebar" /> のようなpropを追加し始めます。コンポーネントは急速に肥大化し、あらゆる配置パターンを内部で知る必要が出てきてしまいます。
これらの問題の根本原因は、コンポーネントが「自身の見た目(内側のスタイル)」と「自身がどう配置されるか(外側のスタイル)」という、異なる責務を同時に持とうとしている点にあります。
解決策:「C層は内側、P層は外側」という責務の分離
この問題を解決するための核心的なルールは、「スタイルの責務を明確に分離する」ことです。FLOCSSのレイヤー構造は、この責務の分離を実践するのに最適です。
核心ルール:「Component層」は自身の`width`や`margin`を定義しない
私たちは、Next.jsプロジェクトにおいて以下の厳格なルールを設けます。
C層(Component)は、
padding,color,font-size,borderといった「内側」のスタイルのみを定義する。C層は、
width,height,margin,position,top/left/right/bottomといった「外側」(レイアウト)のスタイルを原則として定義しない。「外側」のスタイルは、そのコンポーネントを配置する親、すなわち L層(Layout)または P層(Project)が定義する。
これにより、C-Buttonは、どこに置かれても「ボタンらしい見た目」を保ちつつ、親(P層)によって「幅200pxにされたり」「flex-itemとして振る舞わされたり」することを許容する、真に再利用可能で柔軟なコンポーネントになります。
FLOCSS + CSS Modules + BEM の最適な共存方法
このルールを徹底するため、3つの技術を以下のように使い分けます。
FLOCSS (フロックス)
styles/ディレクトリ内のファイル構成(責務の分類)として利用します。(例: styles/F/ (Foundation), styles/L/ (Layout), styles/O/ (Object), styles/C/ (Component), styles/P/ (Project))
CSS Modules
Next.jsの標準機能。FLOCSSの各レイヤー(特に C層 と P層)で作成する .module.scss ファイルに適用し、クラス名をローカルスコープに閉じ込めます。
BEM (ベム)
CSS Modules(.module.scss)のファイル内部での命名規則として、簡略化して使用します。CSS Modulesがスコープを保証するため、冗長なBlock名は不要です。.block__element--modifier の代わりに .element や .modifier だけで十分機能します。
例えば、C-Button.module.scss 内では以下のようになります。
/*
* C-Button.module.scss
* Block名はファイル名(Button)が担当
*/
/* .button (Block) */
.button {
display: inline-flex;
/* ... 内側のスタイル (padding, background-color, etc.) */
/* ここに width や margin は書かない */
}
/* .icon (Element) */
.icon {
margin-right: 8px;
}
/* .primary (Modifier) */
.primary {
background-color: blue;
color: white;
}
実践:Next.jsにおける具体的な実装パターン
「C層が`width`を持たない」ルールを、実際のコードで見ていきましょう。
パターン1:C層コンポーネント(`C-Button`)の実装
コンポーネント自身は、渡されたclassName(親が定義したレイアウトクラス)を受け入れられるように設計します。
/* src/components/C/Button/C-Button.tsx */
import styles from './C-Button.module.scss';
import cn from 'classnames'; // classnamesライブラリの利用を推奨
type Props = {
children: React.ReactNode;
isPrimary?: boolean;
className?: string; // 親からレイアウトクラスを受け取るためのprop
};
export const Button = ({ children, isPrimary, className }: Props) => {
// 1. 自身の内側スタイル (styles.button)
// 2. 自身の状態スタイル (styles.primary)
// 3. 親から渡された外側スタイル (className)
const buttonClasses = cn(
styles.button,
{ [styles.primary]: isPrimary },
className // ここが重要
);
return (
<button className={buttonClasses}>
{children}
</button>
);
};
パターン2:P層(`P-Home`)がC層コンポーネントのレイアウトを定義する方法
次に、この`Button`をトップページ(P層)で使うケースです。P層は「Buttonを幅300pxで中央に配置したい」と考えます。
/* src/styles/P/Home/P-Home.module.scss */
.hero {
padding: 60px 0;
}
/* * .hero__actionButton
* P層が「C-Button」の外側レイアウトを定義するクラス
*/
.hero__actionButton {
display: block;
width: 300px;
margin: 40px auto 0;
text-align: center;
}
/* src/app/page.tsx (P-Home) */
import { Button } from '@/components/C/Button/C-Button';
import styles from '@/styles/P/Home/P-Home.module.scss'; // P層のスタイル
export default function Home() {
return (
<main>
<section className={styles.hero}>
<h1>Welcome to Our Site</h1>
{/*
* C-Buttonに、P層で定義したレイアウトクラス (styles.hero__actionButton) を
* className prop 経由で渡す
*/}
<Button
isPrimary
className={styles.hero__actionButton}
>
Learn More
</Button>
</section>
</main>
);
}
この設計により、C-Buttonコンポーネントのコード(.tsx, .module.scss)は一切変更することなく、P層の要求(幅300px, 中央寄せ)に応えることができました。これこそが真の「責務の分離」と「再利用性」です。
効率化:`pxToRem()`ユーティリティの導入
この厳格なルールに加え、もう一つの課題「単位の管理」を解決します。
なぜ`rem`か? なぜ`pxToRem`関数が必要か?
rem (root em) は、ルート(<html>)のフォントサイズを基準とする相対単位です。ユーザーがブラウザの文字サイズ設定を変更した場合、remで指定された要素(フォントサイズ、余白、幅)は適切にスケーリングされ、アクセシビリティが向上します。
しかし、Figmaなどのデザインツールでは、通常`px`(ピクセル)単位でデザインされます。開発者が毎回「12pxは 12/16 = 0.75rem」と暗算するのは非効率的で、ミスも生みます。
そこで、Sass/SCSSのカスタム関数 pxToRem() を導入します。
SCSS/Sassでの`pxToRem`関数の実装とグローバルな設定
まず、FLOCSSのF(Foundation)またはO(Object)レイヤーに、ツール(関数)を定義します。
/* styles/F/foundation/_tools.scss */
@use 'sass:math';
/*
* ブラウザのデフォルトフォントサイズ(またはhtmlのフォントサイズ)
* Next.jsのglobals.cssでhtml { font-size: 100%; } (16px)
* または 62.5% (10px) に設定する戦略と連動させます。
* ここでは 16px をベースとします。
*/
$base-font-size: 16;
@function pxToRem($pixels) {
// 単位が 'px' の場合は数値を取り出し、単位がない場合はそのまま使用
@if unit($pixels) == 'px' {
$pixels: math.div($pixels, 1px);
}
@if (unitless($pixels)) {
// ($pixels / $base-font-size) * 1rem
@return math.div($pixels, $base-font-size) * 1rem;
} @else {
@error "pxToRem() expects a unitless number or a px value. Got #{$pixels}.";
}
}
次に、Next.jsのnext.config.js(または.mjs)で、この関数(と他のグローバルSCSSファイル)を全ての.module.scssで自動的に読み込めるように設定します。
/* next.config.js */
const path = require('path');
/** @type {import('next').NextConfig} */
const nextConfig = {
// ...他の設定
sassOptions: {
includePaths: [path.join(__dirname, 'styles')],
// @use 'F/foundation/tools' as t; と書けるように
prependData: `@use 'F/foundation/_tools.scss' as t; @use 'F/foundation/_variables.scss' as v;`,
},
};
module.exports = nextConfig;
pxToRem関数の使用例
これにより、C-Button.module.scss 内で、デザインカンプの値をそのまま関数に渡すことができます。
/* C-Button.module.scss */
/* prependDataにより、@use 'F/foundation/tools' as t; は不要 */
.button {
padding: t.pxToRem(12) t.pxToRem(24); /* 12px 24px を指定 */
font-size: t.pxToRem(16); /* 16px を指定 */
border-radius: t.pxToRem(8); /* 8px を指定 */
/* C層だが、最小幅だけは持ちたい場合(例外ルール)*/
min-width: t.pxToRem(150);
}
開発者は`px`値で思考でき、出力はアクセシブルな`rem`値となり、保守性と開発効率が劇的に向上します。
責務の分離こそが、破綻しないCSS設計の鍵
Next.jsにおけるCSS設計の破綻は、技術(CSS ModulesやBEM)の問題ではなく、ルールの問題(責務の分離ができていない)であることが大半です。
「C層は`width`を持たず、P層/L層がレイアウトを定義する」というルールを徹底し、pxToRem関数で単位を統一することで、あなたのCSSアーキテクチャは驚くほどクリーンで、再利用性が高く、スケーラブルになります。
モダンなフロントエンド開発におけるCSSアーキテクチャの構築や、既存コードのリファクタリングにお悩みでしたら、ぜひ一度ご相談ください。

