React でレイアウトコンポーネントを設計する

概要

React でアプリを作っていると、画面の多くは「ヘッダー」「本文」「カード一覧」「余白」といった似た構造の繰り返しになります。これらを毎回 div と className でベタ書きすると、画面が増えるほど余白や間隔の指定がバラバラになり、デザインの一貫性が崩れていきます。

本記事では、そうした繰り返しをレイアウトコンポーネントとして切り出し、見た目を再利用可能にする入門的な設計を扱います。React のコンポーネント分割はできるが、レイアウトの整理がうまくいかない、という段階の方を対象にしています。

コンポーネントを2種類に分けて考える

UI コンポーネントを次の2種類に分けると整理しやすくなります。

  • 見た目を持つコンポーネント(Button, Card, Avatar など)… それ自体が意味を持つ部品
  • 配置を担うコンポーネント(Stack, Container, Grid など)… 中身は問わず、子要素の並びと間隔だけを決める

後者が「レイアウトコンポーネント」です。自分では装飾を持たず、children をどう配置するかだけに責任を持ちます。この分離ができると、「余白を変えたい」ときに触る場所が1か所に集約されます。

よくある課題:余白が各画面に散らばる

まず、ありがちなコードです。

function ProfilePage() {
  return (
    <div style={{ padding: 24 }}>
      <h1 style={{ marginBottom: 16 }}>プロフィール</h1>
      <div style={{ marginBottom: 16 }}>
        <Avatar />
      </div>
      <div style={{ marginBottom: 16 }}>
        <p>自己紹介テキスト</p>
      </div>
      <button>編集する</button>
    </div>
  );
}

このコードは次の問題を抱えています。

  • marginBottom: 16 が要素ごとに繰り返されている
  • 最後の要素にだけ margin を付け忘れる/付けすぎるミスが起きやすい
  • 別の画面では間隔が 12 になっていたりして揃わない
  • 全体の余白変更が全要素の手修正になる

間隔という配置の責務が、各ページのコンポーネントに溶け込んでいるのが原因です。

対応方針:配置の責務をコンポーネントに閉じ込める

配置専用のコンポーネントを用意し、ページ側から「どう並べるか」の記述を追い出します。入門として押さえたいのは3つです。

  1. Stack … 子要素を縦(または横)に一定間隔で並べる
  2. Container … 本文の最大幅を制限し、中央寄せする
  3. Grid … 2次元の格子状に並べる

実装例

Stack:一定間隔で積む

import { type ReactNode } from "react";

type StackProps = {
  children: ReactNode;
  gap?: number;          // 要素間の間隔(px)
  direction?: "row" | "column";
};

export function Stack({ children, gap = 16, direction = "column" }: StackProps) {
  return (
    <div style={{ display: "flex", flexDirection: direction, gap }}>
      {children}
    </div>
  );
}

ポイントは、要素ごとの margin をやめ、親の gap で間隔を表現することです。gap は flex / grid の標準機能で、「最後の要素だけ余白がはみ出す」問題が原理的に起きません。

function ProfilePage() {
  return (
    <Stack gap={16}>
      <h1>プロフィール</h1>
      <Avatar />
      <p>自己紹介テキスト</p>
      <button>編集する</button>
    </Stack>
  );
}

各要素から余白の記述が消え、「縦に16px間隔で積む」という意図だけが残りました。

Container:本文幅を制御する

type ContainerProps = {
  children: ReactNode;
  maxWidth?: number;     // 本文の最大幅(px)
};

export function Container({ children, maxWidth = 768 }: ContainerProps) {
  return (
    <div
      style={{
        maxWidth,
        marginInline: "auto",   // 左右マージンを自動で中央寄せ
        paddingInline: 16,      // 画面端に貼り付かないための左右余白
      }}
    >
      {children}
    </div>
  );
}

横に広いディスプレイで本文が端から端まで伸びるのを防ぎ、中央寄せします。

Grid:格子状に並べる

type GridProps = {
  children: ReactNode;
  minItemWidth?: number;   // 各要素の最小幅(px)
  gap?: number;
};

export function Grid({ children, minItemWidth = 240, gap = 16 }: GridProps) {
  return (
    <div
      style={{
        display: "grid",
        gap,
        gridTemplateColumns: `repeat(auto-fill, minmax(${minItemWidth}px, 1fr))`,
      }}
    >
      {children}
    </div>
  );
}

repeat(auto-fill, minmax(...)) を使うと、画面幅に応じて列数が自動で増減します。メディアクエリを書かずに簡易なレスポンシブが実現でき、入門段階では十分です。

組み合わせてページ骨格を作る

function DashboardPage() {
  return (
    <Container maxWidth={960}>
      <Stack gap={24}>
        <h1>ダッシュボード</h1>
        <Grid minItemWidth={240} gap={16}>
          <Card title="売上" />
          <Card title="訪問数" />
          <Card title="コンバージョン" />
        </Grid>
      </Stack>
    </Container>
  );
}

「中央寄せの960px幅の中に、24px間隔で見出しとカードグリッドを積む」という構造が、スタイルの詳細を見なくても読めるようになりました。

間隔の値は定数にまとめる

gap={16} のように数値を直接書くと、プロジェクトが育つにつれ中途半端な値が混ざり始めます。間隔はスケール(段階)として定義し、そこから選ぶようにします。

export const space = { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 } as const;
type Space = keyof typeof space;
type StackProps = {
  children: ReactNode;
  gap?: Space;
  direction?: "row" | "column";
};

export function Stack({ children, gap = "md", direction = "column" }: StackProps) {
  return (
    <div style={{ display: "flex", flexDirection: direction, gap: space[gap] }}>
      {children}
    </div>
  );
}

gap に渡せる値が "xs" | "sm" | "md" | "lg" | "xl" に限定され、任意の数値を書けなくなります。デザインの一貫性を型で守れるのが利点です。

確認方法

  • Stack の gap を1か所変えて、配下の全要素の間隔が一斉に変わることを確認する
  • 画面幅を縮めて Grid の列数が自動で減ることを確認する
  • ページ側のコンポーネントから margin / padding の直書きが消えているかを確認する

注意点

  • レイアウトコンポーネントに色やフォントを持たせない(配置と装飾を混ぜない)
  • 何でも受け取る巨大 Box を1つ作らない。用途ごとに Stack / Container / Grid と小さく分ける
  • Tailwind や CSS Modules を使っているなら、style ではなくそれらの手法で同じ考え方を適用する

まとめ

  • 画面の繰り返し構造は、配置専用のレイアウトコンポーネントに切り出す
  • 要素ごとの margin をやめ、親の gap で間隔を表現すると崩れにくい
  • Stack / Container / Grid の3つで多くの画面骨格はカバーできる
  • 間隔は数値直書きではなくスケール(トークン)にまとめ、型で選択肢を縛ると一貫性が保てる

レイアウトを部品化しておくと、画面が増えても余白の調整は1か所で済みます。まずは Stack から導入してみてください。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。