【Diseño CSS】Función pxToRem y la regla de que 'la capa C no tiene ancho'. Cómo prevenir fallos con Next.js + FLOCSS

【Diseño CSS】Función pxToRem y la regla de que 'la capa C no tiene ancho'. Cómo prevenir fallos con Next.js + FLOCSS のビジュアル

¿El diseño CSS en Next.js se está desmoronando a medida que se repite la reutilización de componentes?
Al combinar FLOCSS, CSS Modules y BEM, y establecer una regla estricta de 'no especificar ancho o margen en la capa de Componente', puede construir una estrategia CSS verdaderamente reutilizable.
Junto con la optimización de la eficiencia del desarrollo mediante la función pxToRem, se explica un método de estilo práctico.

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

¿Fue útil este artículo?