はじめに
コンポーネント設計は、モダンなフロントエンド開発において欠かせない要素です。適切に設計されたコンポーネントは、開発の効率を向上させるだけでなく、保守性やスケーラビリティにも大きく貢献します。
本記事では、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のユーティリティを結合する関数
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
の状態を取得・更新可能ThemeProvider
をApp
の上位に配置することで、全体にテーマ設定を適用
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} />
);
};
ポイント
variant
やsize
を指定するだけで異なるデザインにできる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>
);
};
ポイント
- より柔軟な使い方をしたい場合として、
header
やfooter
のスロットを追加
カスタムフックを活用する
コンポーネントのロジックをカスタムフックとして切り出すことで、他のコンポーネントでも同じロジックを使い回せるようになります。
✅ 具体例: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>()
のジェネリクスを使うことで、様々なデータ型でも利用できるloading
やerror
の状態を管理し、コンポーネント側の実装をシンプルにできる- 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 が直感的になるlabel
をprops
に持たせることで、タブのタイトルを明示的に指定可能- コンポーネントの見た目をカスタマイズしやすい
スタイルガイドラインの整備(UIの一貫性を保つための指針作成)
スタイルガイドラインは、プロジェクト全体でデザインの統一感を維持し、開発者間での認識のズレを防ぐための重要なドキュメントです。以下のポイントを中心に策定すると、UIの一貫性を高め、開発効率を向上させることができます。
テーマの統一
UIの統一感を保つために、カラーパレット、フォント、スペーシングなどのデザイン要素を明確に定めます。
- カラーパレット
- プライマリーカラー、セカンダリーカラー、アクセントカラーを定義
- ライトモード・ダークモードのカラースキームを考慮
- 色の用途(ボタン、背景、テキスト)を明文化
例: Tailwind のカスタムテーマ
module.exports = {
theme: {
extend: {
colors: {
primary: "#1E40AF", // ブルー系のメインカラー
secondary: "#9333EA", // アクセントカラー
background: "#F9FAFB", // 背景色
text: "#111827", // テキストカラー
},
},
},
};
- フォント
- ベースフォントとタイトルフォントを指定
- フォントサイズ、行間、字間を統一
- 読みやすさを考慮(Google Fontsやシステムフォントを活用)
例: Tailwind のフォント設定
module.exports = {
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'Helvetica', 'Arial', 'sans-serif'],
heading: ['Poppins', 'sans-serif'],
},
},
},
};
- スペーシング
- コンポーネント間の余白を統一
- 8pxグリッドシステムを採用(4px刻みも可)
例: Tailwind のスペーシング設定
module.exports = {
theme: {
extend: {
spacing: {
'4': '16px',
'8': '32px',
'12': '48px',
},
},
},
};
デザインシステムと連携
デザインの統一を保ち、開発者とデザイナーがスムーズに連携できるように、デザインシステムの活用が重要です。
- Figma
- コンポーネントライブラリを作成し、デザイナーと開発者が共通のUIパーツを利用
- スタイル(色、フォント、アイコン、間隔)を明文化
- Storybook
- コンポーネントをドキュメント化し、開発者がスタイルを正しく適用できるようにする
- UIの変更が即座に確認可能
スタイルの管理手法の統一
チーム全体で一貫したスタイル管理を行うため、適切な手法を選択します。
Tailwind CSS のユーティリティクラスを活用
- クラスベースのスタイリングにより、スコープの競合を回避
@apply
を使用してコンポーネントレベルでスタイルを共通化
例: Tailwind でボタンコンポーネントを統一
@layer components {
.btn-primary {
@apply px-4 py-2 bg-primary text-white rounded-md hover:bg-opacity-80;
}
}
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 を活用することで、簡単にダークモードやカスタムテーマを適用できます。
- ThemeProvider のセットアップ
next-themes
を使用して、テーマの状態を管理し、ページ全体に適用します。
import { ThemeProvider } from 'next-themes';
export default function MyApp({ Component, pageProps }) {
return (
<ThemeProvider attribute="class" defaultTheme="light">
<Component {...pageProps} />
</ThemeProvider>
);
}
- Tailwind CSS の設定
Tailwind の theme 設定をカスタマイズし、コンポーネントごとに適用できるようにします。
// tailwind.config.js
module.exports = {
darkMode: 'class', // ダークモードをクラスベースで適用
theme: {
extend: {
colors: {
primary: '#1e3a8a', // ブランドカラー
secondary: '#9333ea',
},
},
},
};
- コンポーネントでのテーマ適用
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-*
属性を適切に使用する- キーボード操作を考慮する
- コントラスト比を確保する
アクセシビリティを考慮した設計の具体的なポイント
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>
- キーボード操作を考慮する
マウスが使えないユーザーでも操作できるように、キーボード操作をサポートする。
- フォーカス可能な要素
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);
}, []);
- コントラスト比の確保
色覚異常のあるユーザーや視力が弱いユーザーのために、十分なコントラストを確保する。
推奨コントラスト比
- 通常のテキスト: 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
- フォームのアクセシビリティ
ラベルを適切に関連付ける
- label 要素を input に関連付ける
<label htmlFor="email">メールアドレス</label>
<input type="email" id="email" />
- エラーメッセージを適切に伝える
aria-live
を使用してリアルタイムにエラーメッセージを通知
<p id="error-message" aria-live="polite">
メールアドレスを入力してください。
</p>
aria-live
の役割
- 動的に変化するコンテンツ(エラーメッセージやロード状態など)を、スクリーンリーダーが適切なタイミングで読み上げるようにするため に使う。
- 例えば、フォームのエラーメッセージや、検索結果の更新などに適用することで、視覚的な変化を音声で伝えられる。
- モーダルのアクセシビリティ
モーダルを開いた際、フォーカスをモーダル内に閉じ込めることで、適切な操作をサポート。
例)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の動作を変えずにバグ修正を行った場合
{
"version": "2.1.4",
"dependencies": {
"@my-ui/button": "^2.1.0"
}
}
上記の場合
2.1.4
→ メジャーバージョン2、マイナーバージョン1、パッチバージョン4@my-ui/button
の^2.1.0
→2.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
に以下を追加
module.exports = {
addons: ['@storybook/addon-docs'],
};
コンポーネントに JSDoc コメントを付けることで、Docs ページに自動的に反映できます。
/**
* 基本的なボタンコンポーネント
* @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による柔軟なスタイル設計 など、ツールを適切に組み合わせることで、開発効率を最大化できます。
コンポーネント設計は一度決めたら終わりではなく、プロジェクトの成長に応じて進化させる必要があります。本記事の内容を参考にしながら、チームの開発スタイルやプロダクトに適した形で取り入れていただければ幸いです。
関連する技術ブログ
Webアクセシビリティの完全ガイド:Lighthouse / axe による自動テスト、WCAG基準策定、キーボード操作・スクリーンリーダー対応まで
Webアクセシビリティの課題を解決するための包括的なガイド。Lighthouse / axe を活用した自動テストの設定、WCAGガイドラインに基づく評価基準の整備、キーボード操作やスクリーンリーダー対応の改善、カラーコントラストの最適化、ARIAランドマークの導入、フォームやモーダルの操作性向上まで詳しく解説。定期的なアクセシビリティレポートを活用し、継続的な改善を実現する方法も紹介します。
shinagawa-web.com
Mock Service Worker (MSW) を使ったAPIモックとテストの効率化
MSW(Mock Service Worker)を使用して、フロントエンド開発やテスト環境でのAPIモックを効率的に行う方法を解説します。Mock Service Workerの基本的な使い方から、Jestテストでの活用方法、さらにテストを簡単にするための設定手順を紹介します。
shinagawa-web.com
Next.jsとAuth.jsで認証機能を実装するチュートリアル
Next.jsでアプリケーションを作る時に必要となる認証機能をどのように実装するかをご紹介する記事となります。アカウント登録から始まり、ログイン、ログアウト、ページごとのアクセス制御、OAuth、二要素認証、パスワードリセットなど認証に関連する様々な機能をコードベースでご紹介します。
shinagawa-web.com
Next.jsでのメール認証処理の実装ガイド:アカウント登録からトークン検証まで
Next.jsを活用したメール認証の実装方法を解説。アカウント登録時のトークン発行から、Sendgridを使ったメール送信処理まで、具体的な手順を紹介します。
shinagawa-web.com
Next.jsでのメール認証処理の実装ガイド:トークン検証からログイン画面へのリダイレクト処理までの詳細解説
前回、トークンを生成し認証用のメールを送信する処理を実装しました。今回はその続きの処理を実装していきます。ユーザー登録後、メール認証のためのトークン検証処理を実装し、成功・エラー処理を分けてユーザーに適切なメッセージを表示する方法を紹介します。ReactとNext.jsを活用したシンプルかつ堅牢な認証機能の流れを解説し、実際の動作確認まで行います。
shinagawa-web.com
Next.jsを活用したGitHubとGoogleのOAuth認証実装完全ガイド — スムーズなユーザーログインの実現方法
本記事では、Next.jsを使用してGitHubとGoogleのOAuth認証を実装する方法を詳しく解説します。OAuthの基礎から、実際のコードの書き方、トークン管理、認証後のユーザー管理に至るまで、実践的なステップを順を追ってご紹介します。ユーザーの利便性を高める認証機能をスムーズに実装するための完全ガイドです。
shinagawa-web.com
Next.jsでログイン画面を作ってメールアドレス/パスワードでログインできるようにする
Next.jsとAuth.jsを活用して、メールアドレスとパスワードによるログイン機能を実装する方法を解説します。Zodによるバリデーション、データベースアクセス、セッション管理、エラーハンドリングなどの実装手順を詳しく紹介。設定画面へのリダイレクトやログアウト処理もカバーしています。
shinagawa-web.com
Next.jsとmicroCMSで作るブログ:ヘッドレスCMSによるコンテンツ管理と表示
本記事では、Next.jsとmicroCMSを活用したブログ作成方法を解説します。microCMSのヘッドレスCMSを使ってコンテンツ管理を行い、APIを通じてNext.jsでブログ情報を表示する手順を紹介。サーバーレスアーキテクチャを活かした効率的な開発が可能です。
shinagawa-web.com