React × Tailwind CSS × Emotionで実践するコンポーネント設計ガイド:デザインシステム・状態管理・再利用性の最適解

2024/11/22に公開

はじめに

コンポーネント設計は、モダンなフロントエンド開発において欠かせない要素です。適切に設計されたコンポーネントは、開発の効率を向上させるだけでなく、保守性やスケーラビリティにも大きく貢献します。

本記事では、React、Tailwind CSS、Emotion、Storybook、Figma、Next.js などの技術スタックを活用しながら、コンポーネント設計のベストプラクティスを解説します。以下のようなポイントを軸に、デザインシステムに基づいた開発を進めるための指針を紹介します。

  • コンポーネントの設計ガイドライン作成(デザインシステムに基づく命名規則)
  • コンポーネントの状態管理(props、state、context の適切な使用法)
  • コンポーネントの再利用性を高めるための抽象化
  • スタイルガイドラインの整備(UI の一貫性を保つための指針作成)
  • テーマ設定の適用(テーマに基づいたスタイル管理)
  • アクセシビリティ対応を組み込む(コンポーネントごとにアクセシビリティをチェック)
  • コンポーネントのバージョン管理(適切なバージョン管理を導入)
  • コンポーネントのドキュメント作成(利用者向けの詳細な使用方法ガイド)

コンポーネント設計をしっかりと整備することで、プロジェクトの開発スピードを向上させるだけでなく、UIの一貫性やユーザビリティの向上にもつながります。これから紹介する内容を参考に、効率的でメンテナブルなコンポーネント設計を実践していきましょう。

コンポーネントの設計ガイドライン(デザインシステムに基づく命名規則)

デザインシステムに基づき、一貫性と再利用性の高いコンポーネントを設計するための命名規則とベストプラクティスを紹介します。Tailwind CSS または Emotion を活用し、柔軟なスタイリングを可能にします。

命名規則の基本方針

Atomic Design を活用し、コンポーネントを適切な粒度で分類します。

  • Atoms
    最小単位のUIコンポーネントで、単体では機能を持たず、他のコンポーネントと組み合わせて使用します。

      • Button.tsx
      • Input.tsx
      • Icon.tsx
      • Typography.tsx
  • Molecules
    複数のAtomsを組み合わせて、一つの機能を持たせたコンポーネント。

      • SearchBar.tsx(Input + Button)
      • UserCard.tsx(Avatar + Text)
      • FormField.tsx(Label + Input)
  • Organisms
    Moleculesを組み合わせ、ページの主要な構成要素となる大きなコンポーネント。

      • Header.tsx(Logo + NavigationMenu + SearchBar)
      • Sidebar.tsx(UserProfile + NavigationLinks)
      • ProductList.tsx(ProductCardの集合)

Tailwind CSS を活用したコンポーネントの実装例

Button コンポーネント

// components/atoms/Button.tsx
import { cn } from "@/utils/cn"; // Tailwindのユーティリティを結合する関数

type ButtonProps = {
  variant?: "primary" | "secondary";
  children: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;

export const Button = ({ variant = "primary", children, className, ...props }: ButtonProps) => {
  return (
    <button
      className={cn(
        "px-4 py-2 rounded-md text-white font-medium transition",
        variant === "primary" && "bg-blue-500 hover:bg-blue-600",
        variant === "secondary" && "bg-gray-500 hover:bg-gray-600",
        className
      )}
      {...props}
    >
      {children}
    </button>
  );
};

ポイント

  • cn() 関数で className を動的に結合
  • variant プロパティでスタイルを切り替え
  • ButtonHTMLAttributes<HTMLButtonElement> で標準の button 属性を継承

Tailwindのユーティリティを結合する関数

utils.ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

UserCard コンポーネント

// components/molecules/UserCard.tsx
import { Button } from "../atoms/Button";

type UserCardProps = {
  name: string;
  avatarUrl: string;
};

export const UserCard = ({ name, avatarUrl }: UserCardProps) => {
  return (
    <div className="flex items-center p-4 border border-gray-300 rounded-lg shadow-sm">
      <img src={avatarUrl} alt={name} className="w-12 h-12 rounded-full" />
      <div className="ml-3">
        <h3 className="text-lg font-semibold">{name}</h3>
        <Button variant="primary" className="mt-2">Follow</Button>
      </div>
    </div>
  );
};

ポイント

  • border border-gray-300 rounded-lg shadow-sm でカードの枠線と影を設定
  • w-12 h-12 rounded-full でアバター画像を円形に

Emotion を活用したコンポーネントの実装例

Button コンポーネント

import { css } from "@emotion/react";

type ButtonProps = {
  variant?: "primary" | "secondary";
  children: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;

const buttonBaseStyle = css`
  padding: 8px 16px;
  border-radius: 4px;
  font-size: 16px;
  font-weight: 500;
  color: white;
  transition: background-color 0.2s;
  border: none;
  cursor: pointer;
`;

const variantStyles = {
  primary: css`
    background-color: #007bff;
    &:hover {
      background-color: #0056b3;
    }
  `,
  secondary: css`
    background-color: #6c757d;
    &:hover {
      background-color: #5a6268;
    }
  `,
};

export const Button = ({ variant = "primary", children, ...props }: ButtonProps) => {
  return (
    <button css={[buttonBaseStyle, variantStyles[variant]]} {...props}>
      {children}
    </button>
  );
};

ポイント

  • @emotion/react を使い、css プロパティでスタイルを定義
  • variant に応じて動的にスタイルを変更

コンポーネントの状態管理(props、state、context の適切な使用法)

コンポーネントの状態管理は、React の設計において非常に重要です。それぞれのアプローチの特性を理解し、適切に使い分けることで、コードの可読性や保守性を向上させることができます。

props の使用場面

親コンポーネントから子コンポーネントにデータを渡す場合に props を使用します。
基本的に「渡されたデータは変更しない(イミュータブル)」というルールがあります。

適切な使用例

  • UI の表示に必要なデータを渡す
  • 親コンポーネントの状態や関数を子に渡す
type ButtonProps = {
  label: string;
  onClick: () => void;
};

const Button = ({ label, onClick }: ButtonProps) => {
  return <button onClick={onClick}>{label}</button>;
};

// 親コンポーネント
const App = () => {
  const handleClick = () => {
    console.log("ボタンがクリックされました");
  };

  return <Button label="Click Me" onClick={handleClick} />;
};

ポイント

  • label は 表示用データ → props で渡すのが適切
  • onClick は イベントハンドラ → props で渡して、子コンポーネント内で利用

props を使うべきケース
✅ 親から子にデータを渡すとき
✅ 変更の必要がないデータの受け渡し
✅ 関数を子コンポーネントに渡すとき

state の使用場面

state は、コンポーネント内部で変更されるデータ を管理するために使用します。

適切な使用例

  • ユーザーの操作によって変更されるデータ
  • コンポーネント内部でのみ影響を与えるデータ
const Modal = () => {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Open Modal</button>
      {isOpen && (
        <div>
          <p>モーダルが開いています</p>
          <button onClick={() => setIsOpen(false)}>Close</button>
        </div>
      )}
    </div>
  );
};

ポイント

  • isOpen は ユーザー操作で変化する値 → state で管理するのが適切
  • setIsOpen を使って状態を更新し、UI を変更

state を使うべきケース
✅ コンポーネント内部で変更されるデータ
✅ ユーザーの操作で変化するデータ
✅ 一時的なデータ(フォームの入力値など)

context の使用場面

context は、複数のコンポーネント間で共有するデータ を管理するために使います。
props の「バケツリレー(prop drilling)」を避けるために活用します。

適切な使用例

  • アプリ全体で共有するテーマや認証情報
  • 複数のコンポーネント間で共有するデータ
const ThemeContext = createContext("light");

const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

const ThemedComponent = () => {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <div>
      <p>現在のテーマ: {theme}</p>
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        テーマを切り替え
      </button>
    </div>
  );
};

// アプリ全体で ThemeProvider を使用
const App = () => {
  return (
    <ThemeProvider>
      <ThemedComponent />
    </ThemeProvider>
  );
};

ポイント

  • context を使うことで、props を介さずに theme の状態を取得・更新可能
  • ThemeProviderApp の上位に配置することで、全体にテーマ設定を適用

context を使うべきケース
✅ グローバルに管理したいデータ(認証情報・テーマなど)
✅ 複数のコンポーネントで共有するデータ
✅ prop drilling を避けたいとき

状態管理の適切な選択基準

状態管理手法 どんなときに使うか 具体的な用途
props 親から子にデータを渡す ボタンのラベル、リストデータ、イベントハンドラ
state コンポーネント内部で変更されるデータ フォームの入力値、モーダルの開閉状態
context グローバルに管理したいデータ テーマ、認証情報、言語設定

コンポーネントの再利用性を高めるための抽象化

コンポーネントの再利用性を高めるためには、適切な抽象化を行い、異なる場面でも使い回せるように設計することが重要です。以下に、再利用性を高めるためのポイントを詳しく解説します。

汎用的なAPI設計を意識する

コンポーネントのAPI(プロパティの設計)を汎用的にすることで、さまざまな用途で使えるようになります。

✅ 具体例:Button コンポーネント
variant や size のようなプロパティを持たせることで、同じコンポーネントを異なるスタイルで再利用できます。

type ButtonProps = {
  variant?: "primary" | "secondary" | "outline";
  size?: "small" | "medium" | "large";
} & React.ButtonHTMLAttributes<HTMLButtonElement>;

const Button: React.FC<ButtonProps> = ({ variant = "primary", size = "medium", ...props }) => {
  const baseStyle = "px-4 py-2 font-semibold rounded";
  const variantStyles = {
    primary: "bg-blue-500 text-white",
    secondary: "bg-gray-500 text-white",
    outline: "border border-gray-500 text-gray-500",
  };
  const sizeStyles = {
    small: "text-sm px-2 py-1",
    medium: "text-base px-4 py-2",
    large: "text-lg px-6 py-3",
  };

  return (
    <button className={`${baseStyle} ${variantStyles[variant]} ${sizeStyles[size]}`} {...props} />
  );
};

ポイント

  • variantsize を指定するだけで異なるデザインにできる
  • React.ButtonHTMLAttributes<HTMLButtonElement> を継承することで、標準的な button のプロパティ (onClick など) も渡せる
  • どこでも再利用しやすい

子コンポーネントを受け取るパターンを活用

コンポーネントの中に柔軟に要素を挿入できるようにすることで、さまざまな場面で再利用しやすくなります。

✅ 具体例:Card コンポーネント

type CardProps = {
  title: string;
  children: React.ReactNode;
};

const Card: React.FC<CardProps> = ({ title, children }) => {
  return (
    <div className="border p-4 shadow rounded-lg">
      <h2 className="text-xl font-bold">{title}</h2>
      <div className="mt-2">{children}</div>
    </div>
  );
};

使い方

<Card title="Profile">
  <p>Name: John Doe</p>
  <p>Age: 30</p>
</Card>

ポイント

  • children を活用することで、内容を自由にカスタマイズできる
  • title だけを固定パーツにし、シンプルな API で管理できる
type CardProps = {
  header?: React.ReactNode;
  footer?: React.ReactNode;
  children: React.ReactNode;
};

const Card: React.FC<CardProps> = ({ header, footer, children }) => {
  return (
    <div className="border p-4 shadow rounded-lg">
      {header && <div className="border-b pb-2">{header}</div>}
      <div className="mt-2">{children}</div>
      {footer && <div className="border-t pt-2">{footer}</div>}
    </div>
  );
};

ポイント

  • より柔軟な使い方をしたい場合として、headerfooter のスロットを追加

カスタムフックを活用する

コンポーネントのロジックをカスタムフックとして切り出すことで、他のコンポーネントでも同じロジックを使い回せるようになります。

✅ 具体例:API データ取得用の useFetch

import { useState, useEffect } from "react";

const useFetch = <T,>(url: string) => {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then((res) => {
        if (!res.ok) {
          throw new Error("Failed to fetch data");
        }
        return res.json();
      })
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);

  return { data, loading, error };
};

使い方

const UsersList = () => {
  const { data: users, loading, error } = useFetch<User[]>(
    "https://jsonplaceholder.typicode.com/users"
  );

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {users?.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};

ポイント

  • useFetch<T>() のジェネリクスを使うことで、様々なデータ型でも利用できる
  • loadingerror の状態を管理し、コンポーネント側の実装をシンプルにできる
  • API の変更にも柔軟に対応しやすい

Compound Components パターンを活用する

複数のコンポーネントをひとまとめにして、より柔軟に使える API を設計する方法です。

✅ 具体例:Tabs コンポーネント

const Tabs = ({ children }: { children: React.ReactNode }) => {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <div>
      <div className="flex space-x-2 border-b">
        {React.Children.map(children, (child, index) =>
          React.isValidElement(child) ? (
            <button
              className={`px-4 py-2 ${index === activeIndex ? "border-b-2 border-blue-500" : ""}`}
              onClick={() => setActiveIndex(index)}
            >
              {child.props.label}
            </button>
          ) : null
        )}
      </div>
      <div className="p-4">{React.Children.toArray(children)[activeIndex]}</div>
    </div>
  );
};

const Tab = ({ children }: { children: React.ReactNode }) => <>{children}</>;

使い方

<Tabs>
  <Tab label="Home">ホームの内容</Tab>
  <Tab label="Profile">プロフィールの内容</Tab>
  <Tab label="Settings">設定の内容</Tab>
</Tabs>

ポイント

  • <Tabs> 内に <Tab> をネストすることで、API が直感的になる
  • labelprops に持たせることで、タブのタイトルを明示的に指定可能
  • コンポーネントの見た目をカスタマイズしやすい

スタイルガイドラインの整備(UIの一貫性を保つための指針作成)

スタイルガイドラインは、プロジェクト全体でデザインの統一感を維持し、開発者間での認識のズレを防ぐための重要なドキュメントです。以下のポイントを中心に策定すると、UIの一貫性を高め、開発効率を向上させることができます。

テーマの統一

UIの統一感を保つために、カラーパレット、フォント、スペーシングなどのデザイン要素を明確に定めます。

  1. カラーパレット
    • プライマリーカラー、セカンダリーカラー、アクセントカラーを定義
    • ライトモード・ダークモードのカラースキームを考慮
    • 色の用途(ボタン、背景、テキスト)を明文化

例: Tailwind のカスタムテーマ

tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        primary: "#1E40AF", // ブルー系のメインカラー
        secondary: "#9333EA", // アクセントカラー
        background: "#F9FAFB", // 背景色
        text: "#111827", // テキストカラー
      },
    },
  },
};
  1. フォント
  • ベースフォントとタイトルフォントを指定
  • フォントサイズ、行間、字間を統一
  • 読みやすさを考慮(Google Fontsやシステムフォントを活用)

例: Tailwind のフォント設定

tailwind.config.js
module.exports = {
  theme: {
    extend: {
      fontFamily: {
        sans: ['Inter', 'Helvetica', 'Arial', 'sans-serif'],
        heading: ['Poppins', 'sans-serif'],
      },
    },
  },
};
  1. スペーシング
  • コンポーネント間の余白を統一
  • 8pxグリッドシステムを採用(4px刻みも可)

例: Tailwind のスペーシング設定

tailwind.config.js
module.exports = {
  theme: {
    extend: {
      spacing: {
        '4': '16px',
        '8': '32px',
        '12': '48px',
      },
    },
  },
};

デザインシステムと連携

デザインの統一を保ち、開発者とデザイナーがスムーズに連携できるように、デザインシステムの活用が重要です。

  • Figma
    • コンポーネントライブラリを作成し、デザイナーと開発者が共通のUIパーツを利用
    • スタイル(色、フォント、アイコン、間隔)を明文化
  • Storybook
    • コンポーネントをドキュメント化し、開発者がスタイルを正しく適用できるようにする
    • UIの変更が即座に確認可能

Storybook使用イメージ
Image from Gyazo

スタイルの管理手法の統一

チーム全体で一貫したスタイル管理を行うため、適切な手法を選択します。

Tailwind CSS のユーティリティクラスを活用

  • クラスベースのスタイリングにより、スコープの競合を回避
  • @apply を使用してコンポーネントレベルでスタイルを共通化

例: Tailwind でボタンコンポーネントを統一

styles/globals.css
@layer components {
  .btn-primary {
    @apply px-4 py-2 bg-primary text-white rounded-md hover:bg-opacity-80;
  }
}
components/Button.tsx
const Button = ({ children }: { children: React.ReactNode }) => {
  return <button className="btn-primary">{children}</button>;
};

ポイント

  • @apply を使って ユーティリティクラスを CSS に統合
  • btn-primary クラスを コンポーネントで再利用可能にする
  • 複数のコンポーネントで統一されたスタイルを適用可能

テーマ設定の適用

コンポーネントのスタイルを一元管理することで、デザインの一貫性を確保しつつ、スケーラビリティの高いUIを構築できます。

  • テーマ管理のメリット
    • デザインの統一性
      • すべてのコンポーネントで一貫したスタイルを適用できるため、UXが向上する。
    • 保守性の向上
      • スタイルを一括変更できるため、デザインのアップデートが容易。
    • ダークモードやブランドカラーの適用が簡単
      • theme 設定を利用することで、異なるカラースキームの切り替えがスムーズに行える。

実装例(Tailwind CSS + Theme Provider)

Tailwind CSS の theme 設定と next-themes を活用することで、簡単にダークモードやカスタムテーマを適用できます。

  1. ThemeProvider のセットアップ
    next-themes を使用して、テーマの状態を管理し、ページ全体に適用します。
import { ThemeProvider } from 'next-themes';

export default function MyApp({ Component, pageProps }) {
  return (
    <ThemeProvider attribute="class" defaultTheme="light">
      <Component {...pageProps} />
    </ThemeProvider>
  );
}
  1. Tailwind CSS の設定
    Tailwind の theme 設定をカスタマイズし、コンポーネントごとに適用できるようにします。
// tailwind.config.js
module.exports = {
  darkMode: 'class', // ダークモードをクラスベースで適用
  theme: {
    extend: {
      colors: {
        primary: '#1e3a8a', // ブランドカラー
        secondary: '#9333ea',
      },
    },
  },
};
  1. コンポーネントでのテーマ適用
    useTheme フックを使ってテーマを切り替えられるようにする。
import { useTheme } from 'next-themes';

export default function ThemeSwitcher() {
  const { theme, setTheme } = useTheme();

  return (
    <button
      className="p-2 bg-primary text-white rounded"
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
    >
      {theme === 'dark' ? 'Light Mode' : 'Dark Mode'}
    </button>
  );
}

これにより、ダークモード対応の拡張性を持ったコンポーネントを簡単に管理できます。

アクセシビリティを考慮した設計

コンポーネントを設計する際には、アクセシビリティ(a11y)を考慮することが重要です。

アクセシビリティを高める理由

  • すべてのユーザー(視覚障害者やキーボード操作のユーザーなど)が利用しやすくなる
  • SEO(検索エンジン最適化)にもプラスの影響を与える
  • 法的要件(WCAG準拠など)を満たしやすくなる

アクセシビリティの基本ポイント

  • aria-* 属性を適切に使用する
  • キーボード操作を考慮する
  • コントラスト比を確保する

アクセシビリティを考慮した設計の具体的なポイント

  1. aria-* 属性の適切な使用
    aria-* 属性を適切に設定することで、スクリーンリーダーを利用するユーザーがコンテンツを理解しやすくなります。

主要な aria-* 属性

  • aria-label
    ボタンやリンクなどに視覚的なラベルがない場合、適切な名前を付与
<button aria-label="閉じる">×</button>
  • aria-labelledby
    他の要素の id を参照し、ラベルとして利用
<h2 id="section-title">ユーザー設定</h2>
<p aria-labelledby="section-title">このセクションでは、ユーザー設定を変更できます。</p>
  • aria-describedby
    補足情報を提供する要素を指定
<input type="text" id="username" aria-describedby="username-desc" />
<p id="username-desc">ユーザー名を入力してください(半角英数字のみ)。</p>
  • role 属性の適切な指定
    role="dialog" など、適切な役割を付与することでスクリーンリーダーの動作を向上
<div role="dialog" aria-labelledby="modal-title">
  <h2 id="modal-title">設定</h2>
  <p>ここでアカウント設定を変更できます。</p>
</div>
  1. キーボード操作を考慮する
    マウスが使えないユーザーでも操作できるように、キーボード操作をサポートする。
  • フォーカス可能な要素
    button, a, input, textarea, select などはデフォルトでフォーカス可能
    カスタム要素には tabIndex を指定
<div tabIndex={0}>フォーカス可能な要素</div>
  • keydown イベントでキーボード操作を追加
    例えば、Escape キーでモーダルを閉じる
const handleKeyDown = (event: KeyboardEvent) => {
  if (event.key === "Escape") {
    closeModal();
  }
};

useEffect(() => {
  document.addEventListener("keydown", handleKeyDown);
  return () => document.removeEventListener("keydown", handleKeyDown);
}, []);
  1. コントラスト比の確保
    色覚異常のあるユーザーや視力が弱いユーザーのために、十分なコントラストを確保する。

推奨コントラスト比

  • 通常のテキスト: 4.5:1 以上
  • 大きなテキスト (18px, bold 以上): 3:1 以上

NG例: コントラストが低い

button {
  background-color: lightgray;
  color: white;
}

OK例: コントラスト比を改善

button {
  background-color: blue;
  color: white;
}

開発中のコントラストチェック

  • Chrome DevTools

  • WebAIM Contrast Checker

https://webaim.org/resources/contrastchecker/

  1. フォームのアクセシビリティ
    ラベルを適切に関連付ける
  • label 要素を input に関連付ける
<label htmlFor="email">メールアドレス</label>
<input type="email" id="email" />
  • エラーメッセージを適切に伝える
    aria-live を使用してリアルタイムにエラーメッセージを通知
<p id="error-message" aria-live="polite">
  メールアドレスを入力してください。
</p>

aria-live の役割

  • 動的に変化するコンテンツ(エラーメッセージやロード状態など)を、スクリーンリーダーが適切なタイミングで読み上げるようにするため に使う。
  • 例えば、フォームのエラーメッセージや、検索結果の更新などに適用することで、視覚的な変化を音声で伝えられる。
  1. モーダルのアクセシビリティ
    モーダルを開いた際、フォーカスをモーダル内に閉じ込めることで、適切な操作をサポート。

例)useEffect でフォーカストラップを実装

import { useEffect, useRef } from "react";

const Modal = ({ isOpen, onClose }) => {
  const modalRef = useRef(null);

  useEffect(() => {
    if (!isOpen) return;

    const focusableElements = modalRef.current?.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );

    if (!focusableElements.length) return;

    const firstElement = focusableElements[0];
    const lastElement = focusableElements[focusableElements.length - 1];

    const handleTabKey = (event) => {
      if (event.key === "Tab") {
        if (event.shiftKey && document.activeElement === firstElement) {
          lastElement.focus();
          event.preventDefault();
        } else if (!event.shiftKey && document.activeElement === lastElement) {
          firstElement.focus();
          event.preventDefault();
        }
      }
    };

    document.addEventListener("keydown", handleTabKey);
    return () => document.removeEventListener("keydown", handleTabKey);
  }, [isOpen]);

  if (!isOpen) return null;

  return (
    <div
      ref={modalRef}
      role="dialog"
      aria-modal="true"
      style={{
        position: "fixed",
        top: "50%",
        left: "50%",
        transform: "translate(-50%, -50%)",
        background: "white",
        padding: "20px",
        boxShadow: "0px 4px 6px rgba(0,0,0,0.1)",
      }}
    >
      <h2>モーダルタイトル</h2>
      <p>モーダルの内容です。</p>
      <input type="text" placeholder="入力フィールド" />
      <button onClick={onClose}>閉じる</button>
    </div>
  );
};

export default Modal;
  • フォーカストラップ (Focus Trap) とは?
    モーダルが開いている間、キーボードの Tab キーでのフォーカス移動をモーダル内に閉じ込める 仕組みです。

  • なぜフォーカストラップが必要?
    通常、Tab キーを押すとフォーカス可能な要素間を移動できます。
    しかし、モーダルを開いた状態で Tab を押すと、モーダルの外にフォーカスが移動してしまう可能性があります。

    • モーダルが開く
    • Tab キーを押す
    • モーダルの外(例えばナビゲーションのリンクなど)にフォーカスが移動してしまう
    • モーダルが開いているのに、フォーカスが別の場所へ飛んでしまい操作しづらい
  • フォーカストラップを実装すると

    • Tab キーを押しても モーダル内のフォーカス可能な要素から出られない ようになる。
    • これにより、キーボード操作だけでもモーダルをスムーズに扱える。

コンポーネントのバージョン管理

コンポーネントのバージョン管理を適切に行うことで、プロジェクトの安定性を保ちつつ、安全に機能追加やバグ修正を行えます。ここでは、具体的な管理方法や実践的なアプローチについて詳しく説明します。

バージョン管理の重要性

コンポーネントのバージョン管理を適切に行うことで、以下のメリットがあります。

  • 変更の影響範囲を把握しやすくなる
  • チームメンバー間の認識を統一しやすい
  • レガシーコードのメンテナンスが容易になる
  • 複数プロジェクトでコンポーネントを再利用しやすい

特に、コンポーネントライブラリを開発・提供している場合、適切なバージョン管理は不可欠です。

Semantic Versioning(SemVer)の導入

バージョン管理の基準として、Semantic Versioning(SemVer)を導入すると、変更の種類を明確に分類できます。

SemVer のルール

MAJOR.MINOR.PATCH
  • MAJOR(破壊的変更): 既存のAPIと互換性のない変更を加えた場合
  • MINOR(機能追加): 既存のAPIと互換性を保ちつつ、新しい機能を追加した場合
  • PATCH(バグ修正): 既存のAPIの動作を変えずにバグ修正を行った場合
package.json
{
  "version": "2.1.4",
  "dependencies": {
    "@my-ui/button": "^2.1.0"
  }
}

上記の場合

  • 2.1.4 → メジャーバージョン2、マイナーバージョン1、パッチバージョン4
  • @my-ui/button^2.1.02.1.x まで自動アップデートされる

変更履歴の管理

バージョンごとの変更を CHANGELOG.md に記録することで、開発者や利用者が変更内容を把握しやすくなります。

CHANGELOG.md の書き方(例)

# Changelog

## [2.1.0] - 2025-03-06
### Added
- 新しい `Card` コンポーネントを追加
- `Button` コンポーネントに `loading` プロパティを追加

## [2.0.0] - 2025-02-20
### Breaking Changes
- `Button` コンポーネントの `size` プロパティを `small` / `medium` / `large` に変更
- `Card` コンポーネントの `onClick` イベントの引数を変更

このように、Added、Breaking Changes などのカテゴリを設けると分かりやすくなります。

バージョンとリリースタグの紐付け

Git のタグを活用することで、リリースごとのバージョンを明確に管理できます。

タグの付け方

git tag v2.1.0
git push origin v2.1.0

タグを利用すると、特定のバージョンに対応したコードを簡単にチェックアウトできます。

git checkout tags/v2.1.0

バージョン管理のワークフロー

バージョン管理の運用をスムーズにするための具体的なワークフローを示します。

ワークフロー例

  • 新しい機能追加・修正
    • feature/new-button-loading のようなブランチを作成
    • 修正後、main へマージ
  • バージョン更新
    • package.json の version を更新
    • CHANGELOG.md を更新
    • Git のタグを作成し、main にプッシュ
  • リリース
    • GitHub Releases に変更内容を記載
    • npm publish でライブラリを公開(必要な場合)

コンポーネントのドキュメント作成

利用者(開発エンジニア)がコンポーネントを正しく理解し、効率よく活用できるように、ドキュメントを充実させることは重要です。
特に、プロジェクトが成長するにつれて、統一されたドキュメントがあると、開発者間の認識ズレを防ぎ、再利用性が向上します。

ドキュメントに含めるべき内容

コンポーネントのドキュメントには、以下の内容を記載するのが一般的です。

セクション 説明
概要(Introduction) コンポーネントの目的や用途を簡潔に説明
使用方法(Usage) コンポーネントの基本的な使い方を記述
Propsとデフォルト値 受け取るプロパティの一覧、型、デフォルト値を記載
例(Examples) さまざまなユースケースを示す
スタイル(Styling) TailwindやEmotionなど、スタイルのカスタマイズ方法を説明
アクセシビリティ(Accessibility) WCAG 準拠の方法や aria-* 属性の説明
バージョン履歴(Changelog) 変更点や修正履歴を記載

Storybook を活用したドキュメント作成

Storybook を使うことで、コンポーネントの使用例を視覚的に確認しながら、ドキュメントを作成できます。
インタラクティブに操作できるため、開発者やデザイナーが直感的に理解しやすくなります。

まずは Storybook をプロジェクトに追加します。

npx storybook init

Story ファイルの作成
例えば、Button コンポーネントのストーリーを作成する場合、以下のように Button.stories.tsx を用意します。

import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  argTypes: {
    onClick: { action: 'clicked' },
  },
};

export default meta;

type Story = StoryObj<typeof Button>;

export const Default: Story = {
  args: {
    label: 'Click me',
  },
};

export const Primary: Story = {
  args: {
    label: 'Click me',
    primary: true,
  },
};

Docsアドオンを活用

Storybook の @storybook/addon-docs を利用すると、Markdown や JSX でドキュメントを記述できます。

npm install @storybook/addon-docs

.storybook/main.js に以下を追加

.storybook/main.js
module.exports = {
  addons: ['@storybook/addon-docs'],
};

コンポーネントに JSDoc コメントを付けることで、Docs ページに自動的に反映できます。

Button.tsx
/**
 * 基本的なボタンコンポーネント
 * @param label - ボタンのラベル
 * @param onClick - クリック時のハンドラー
 */
export const Button = ({ label, onClick }: { label: string; onClick: () => void }) => {
  return <button onClick={onClick}>{label}</button>;
};

さいごに

本記事では、Reactを中心とした技術スタックを活用しながら、コンポーネント設計のベストプラクティスを解説しました。デザインシステムに基づいた設計を行うことで、開発の一貫性を保ち、チーム全体の生産性を向上させることができます。

また、状態管理や再利用性、スタイルの統一、アクセシビリティ対応、バージョン管理といった要素を組み合わせることで、スケールするフロントエンドアーキテクチャを構築することが可能です。特に、Storybookを活用したコンポーネントの可視化、Figmaとのデザイン連携、EmotionやTailwind CSSによる柔軟なスタイル設計 など、ツールを適切に組み合わせることで、開発効率を最大化できます。

コンポーネント設計は一度決めたら終わりではなく、プロジェクトの成長に応じて進化させる必要があります。本記事の内容を参考にしながら、チームの開発スタイルやプロダクトに適した形で取り入れていただければ幸いです。

記事に関するお問い合わせ📝

記事の内容に関するご質問、ご意見などは、下記よりお気軽にお問い合わせください。
ご質問フォームへ

技術支援などお仕事に関するお問い合わせ📄

技術支援やお仕事のご依頼に関するお問い合わせは、下記よりお気軽にお問い合わせください。
お問い合わせフォームへ

関連する技術ブログ