はじめに
Webアプリを作っていると、いつの間にか「同じようなボタン」「微妙に違うカード」「コピペで増殖した入力欄」であふれてしまうことはありませんか。最初は気にならなくても、画面が増えるほどデザインはブレ、修正は漏れ、保守コストはじわじわ膨らんでいきます。
この記事では、Webアプリで「共通化を検討するべき要素」を体系的に整理します。どこから手をつければいいか分からない人でも、「この要素は共通化しておくと効く」という判断軸をまとめました。
この記事でわかること:
- なぜUIの共通化が必要なのか(メリットと、やりすぎのリスク)
- 共通化すべき要素をレイヤー(階層)で整理する考え方
- 各コンポーネント(ボタン・入力欄・カード・モーダルなど)で何を統一すべきか
-
Design Tokensを起点にした共通化の進め方 - 「いつ共通化すべきか」の実践的な判断ルール
コード例は React + CSS(CSS変数) で示しますが、考え方はどのフレームワークでも共通です。
なぜ共通化が必要なのか
共通化していないアプリには、次のような問題が起こりがちです。
- デザインのブレ: ボタンの色や角丸が画面ごとに微妙に違う
- 修正漏れ: 「ボタンの色を変えて」の一言で、何十か所も直す羽目になる
- 保守コストの増大: 似たコードが散らばり、どれが正解か分からなくなる
共通化すると、これらが次のように改善されます。
- 一貫性: ユーザーは「同じ見た目=同じ操作」と学習でき、使いやすくなる
- 変更容易性: 1か所直せば全画面に反映される
- 開発速度: 部品を組み合わせるだけで画面が作れる
一方で、「共通化しすぎる」のも危険です。まだ1か所でしか使っていないものを無理に汎用部品にすると、やたらと引数(props)が増えた「何でも屋コンポーネント」が生まれ、かえって読みにくくなります。共通化はあくまで「重複が実際に発生している、または確実に発生する」要素から始めるのがコツです。この判断軸は記事の最後で改めて触れます。
共通化を検討する要素の全体像
共通化すべき要素を「ボタン」「カード」とフラットに並べると、数が多くて混乱します。そこで本記事では、依存関係のレイヤー(階層)で整理します。レイヤーの数字が小さいほど土台で、レイヤーの数字が大きいの層はレイヤーの数字が小さい層を組み合わせて作られます。
この5レイヤーに沿って、それぞれの要素を見ていきましょう。
レイヤー1: Design Tokens(すべての土台)
Design Tokens(デザイントークン) とは、色・影・角丸・余白・フォントサイズといった**「デザインの値そのもの」に名前をつけて一元管理したもの**です。#3b82f6 のような生の値を直接書く代わりに、--color-primary のような名前で参照します。
なぜ最初にこれを決めるべきかというと、この後に作るすべての部品がここを参照するからです。トークンを1か所変えれば、ボタンもカードも入力欄も一斉に変わります。逆にトークンがないと、各部品に色や余白がバラバラに散らばり、共通化の効果が半減します。
:root {
/* 色 */
--color-primary: #3b82f6;
--color-danger: #ef4444;
--color-text: #1f2937;
--color-bg: #ffffff;
--color-border: #e5e7eb;
/* 余白(4の倍数で統一すると揃いやすい) */
--space-1: 4px;
--space-2: 8px;
--space-3: 16px;
--space-4: 24px;
/* 角丸・影 */
--radius-md: 8px;
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
/* フォントサイズ */
--font-sm: 14px;
--font-md: 16px;
--font-lg: 20px;
}
ポイントは、色や余白に「意味の名前」をつけることです。--color-blue ではなく --color-primary とすることで、後からブランドカラーを緑に変えても名前はそのまま使えます。
レイヤー2: 基本UI部品
トークンを土台に、これ以上分解できない最小の部品を作ります。
Title(見出し)
ページタイトル・セクション見出しのフォントサイズや余白を統一します。見出しレベル(h1〜h3)をpropsで切り替えられるようにすると、サイズの揺れを防げます。
PrimaryButton / SecondaryButton / DangerButton / IconButton
ボタンは最も乱立しやすい部品です。役割(variant)でパターン化しましょう。
- PrimaryButton: 保存・送信など主要アクション
- SecondaryButton: キャンセル・戻るなど補助アクション
- DangerButton: 削除など破壊的アクション
- IconButton: アイコンだけの小さな操作ボタン
「保存・キャンセル・削除」以外の補助操作もこのパターンに乗せると、ボタンの見た目が散らからずに済みます。
// variant で見た目を切り替える1つのボタンに集約する
export function Button({ variant = "primary", children, ...props }) {
return (
<button className={`btn btn--${variant}`} {...props}>
{children}
</button>
);
}
.btn {
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-md);
font-size: var(--font-md);
border: none;
cursor: pointer;
}
.btn--primary { background: var(--color-primary); color: #fff; }
.btn--secondary { background: #fff; color: var(--color-text); border: 1px solid var(--color-border); }
.btn--danger { background: var(--color-danger); color: #fff; }
Input / Textarea / Select(入力部品)
入力欄は、高さ・角丸・背景・フォーカス色・エラー表示を統一します。これらがバラバラだとフォーム全体が雑に見えてしまいます。
.input {
height: 40px;
padding: 0 var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg);
font-size: var(--font-md);
}
.input:focus {
outline: none;
border-color: var(--color-primary); /* フォーカス色を統一 */
}
.input--error {
border-color: var(--color-danger); /* エラー表示も共通化 */
}
Badge / StatusLabel
「公開中」「下書き」「エラー」などの状態・カテゴリ・種別表示を統一します。色とラベルの対応をひとつのコンポーネントにまとめると、ステータス表現がアプリ全体で揃います。
Avatar
ユーザーアイコンの表示です。画像がある場合はその画像を、ない場合はイニシャル+背景色を表示する、といったフォールバックや**サイズ(sm/md/lg)**を共通化します。
IconCircle
アイコンを丸い背景に入れる表現です。空状態・プロフィール・カテゴリ表示など、地味ですが意外と多くの場所で使うため、共通化しておくと便利です。
レイヤー3: 複合・コンテナ部品
基本部品を組み合わせた、もう一段大きな部品です。
FormField
入力フォームの1項目を、ラベル・必須表示・補足文・エラーメッセージ・入力部品でひとまとめにしたものです。これを共通化すると、フォームの見た目とアクセシビリティ(ラベルと入力の紐付け)が一気に揃います。
export function FormField({ label, required, hint, error, children }) {
return (
<div className="form-field">
<label className="form-field__label">
{label}
{required && <span className="form-field__required">*</span>}
</label>
{children /* Input や Select が入る */}
{hint && <p className="form-field__hint">{hint}</p>}
{error && <p className="form-field__error">{error}</p>}
</div>
);
}
Card
コンテンツをまとめる箱です。**背景・角丸・影・内側余白(padding)**を統一します。ダッシュボードや一覧で多用するため、早めに共通化したい部品の代表格です。
ListItem / TableRow
一覧表示の1行です。hover時の背景・余白・アクションボタンの配置を統一すると、リストやテーブルの見た目が安定します。
Modal / ConfirmDialog
- Modal: 画面の上に重ねて表示する汎用的なダイアログ。オーバーレイ・中央寄せ・閉じる操作(×ボタンや背景クリック)を共通化します。
- ConfirmDialog: Modalを土台にした「削除確認」「ログアウト確認」専用の確認UI。メッセージと「実行/キャンセル」ボタンだけ渡せば使えるようにすると、確認ダイアログの実装がぐっと楽になります。
ConfirmDialogがModalを内部で使う、というように上のレイヤーが下のレイヤーを再利用するのがレイヤー構造の良いところです。
レイヤー4: レイアウト・状態表示
画面全体の骨格と、「データがない・読込中・エラー」といった状態表示をまとめます。
PageLayout
ページ全体の背景・最大幅・左右余白・上下余白を統一します。これがあると、新しいページを作るたびに余白を調整する手間がなくなります。
Section
ページ内の区切りです。見出し・余白・下線やボーダーをまとめ、セクション間のリズムを揃えます。
EmptyState / LoadingState / ErrorState
データの状態に応じた表示は、毎回その場で書くとバラバラになりがちです。3つのパターンとして共通化しましょう。
- EmptyState: データがない時の「アイコン・見出し・説明文・CTA(次の行動を促すボタン)」
- LoadingState: 読込中の「スピナーやスケルトン・中央寄せ・余白」
- ErrorState: エラー時の「説明文・再試行ボタン」
export function EmptyState({ icon, title, description, action }) {
return (
<div className="empty-state">
<IconCircle>{icon}</IconCircle>{/* レイヤー2の部品を再利用 */}
<h3>{title}</h3>
<p>{description}</p>
{action}
</div>
);
}
この3つを揃えておくと、どの一覧画面でも「同じ作法」で状態を表現でき、ユーザーが迷いません。
レイヤー5: ナビゲーション・操作・通知
最後に、画面をまたいで使われるナビゲーションや通知系です。
Header / NavigationItem
サイト共通のヘッダーと、その中のナビゲーション項目です。アクティブ状態・hover・余白・フォントを統一すると、「今どこにいるか」が分かりやすくなります。
Tabs / SegmentedControl / FilterButton
一覧の絞り込みや表示切替でよく使う選択UIです。見た目は違っても「複数の選択肢から1つ(または複数)を選ぶ」という役割は共通なので、パターン化しておくと使い回せます。
Toast / 通知メッセージ定義
操作結果を一時的に知らせるToast(トースト)です。成功・失敗・警告の表示種別を統一します。あわせて、メッセージの文言(「保存しました」など)も定義として一元管理すると、表現のブレを防げます。
DatePicker / Calendar
日付入力です。日付の入力欄が複数箇所に出るなら共通化対象です。1か所しか使わないなら、無理に共通化せず後回しでも構いません(やりすぎの注意です)。
共通化の進め方(実践Tips)
最後に、「結局いつ共通化すればいいの?」という疑問に答える実践ルールを紹介します。
- 2回出たら共通化を検討、3回出たら共通化する: 1回目はそのまま書き、2回目で「共通化できそう」とメモし、3回目で実際に部品にする。早すぎる抽象化を避けつつ、重複も放置しない、ちょうど良いタイミングです。
-
下のレイヤーから固める: まず
Design Tokensを決め、次に基本部品、最後に複合部品。土台が安定すると上の部品が作りやすくなります。 - propsを増やしすぎない: 「あれもこれも切り替えられる」部品は危険信号です。分岐が増えたら、別コンポーネントに分ける方が読みやすくなります。
-
置き場所を決めておく:
components/ui/(汎用部品)とcomponents/features/(機能固有)を分けるなど、ディレクトリ構成のルールを最初に決めておくと迷いません。
まとめ
この記事では、Webアプリで共通化を検討すべき要素を5つのレイヤーで整理しました。チェックリストとして振り返りましょう。
- レイヤー1 Design Tokens: 色・余白・角丸・影・フォントサイズの値を一元管理する
- レイヤー2 基本UI部品: Title / Button各種 / Input・Textarea・Select / Badge / Avatar / IconCircle
- レイヤー3 複合部品: FormField / Card / ListItem・TableRow / Modal・ConfirmDialog
- レイヤー4 レイアウト・状態: PageLayout / Section / EmptyState・LoadingState・ErrorState
- レイヤー5 ナビ・通知: Header・NavigationItem / Tabs・SegmentedControl・FilterButton / Toast / DatePicker・Calendar
共通化のコツは、下のレイヤー(Design Tokens)から固め、重複が見えてから部品化することです。最初から完璧を目指す必要はありません。「2回出たら検討、3回で共通化」のリズムで、少しずつ整えます。