TOSAKAFUNK

Loading...

【CSS設計】pxToRem関数と「C層はwidthを持たない」ルール。Next.js + FLOCSSで破綻を防ぐ方法

【CSS設計】pxToRem関数と「C層はwidthを持たない」ルール。Next.js + FLOCSSで破綻を防ぐ方法 のビジュアル

Next.jsでのCSS設計が、コンポーネントの再利用を繰り返すうちに破綻していませんか?
FLOCSS, CSS Modules, BEMを組み合わせ、「Component層でwidthやmarginを指定しない」という厳格なルールを設けることで、真に再利用可能なCSS戦略を構築できます。
pxToRem関数による開発効率化と併せて、実践的なスタイリング手法を解説します。

  • CSS破綻の原因コンポーネント(C層)が自身のwidthやmargin(外側のスタイル)を持つと、配置場所(コンテキスト)に依存し再利用性が失われる。
  • 本戦略の核心「C層は内側のスタイル(padding, color)のみ定義し、外側のレイアウト(width, margin)は親(P層やL層)が指定する」という責務の分離。
  • BEMとCSS ModulesFLOCSSでファイルを分類し、CSS Modulesのスコープ内でBEM(Block__Element)を簡潔に使うことで、構造の可読性を高める。
  • pxToRemの導入Sass/SCSSの関数を使い、デザインカンプのpx値を保守性の高いrem値へシームレスに変換し、スケーラビリティを確保する。

なぜ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アーキテクチャの構築や、既存コードのリファクタリングにお悩みでしたら、ぜひ一度ご相談ください。