3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Webアプリで共通化を検討するべき要素 〜UIコンポーネントとDesign Tokensの整理〜

3
Posted at

はじめに

Webアプリを作っていると、いつの間にか「同じようなボタン」「微妙に違うカード」「コピペで増殖した入力欄」であふれてしまうことはありませんか。最初は気にならなくても、画面が増えるほどデザインはブレ、修正は漏れ、保守コストはじわじわ膨らんでいきます。

この記事では、Webアプリで「共通化を検討するべき要素」を体系的に整理します。どこから手をつければいいか分からない人でも、「この要素は共通化しておくと効く」という判断軸をまとめました。

この記事でわかること:

  • なぜUIの共通化が必要なのか(メリットと、やりすぎのリスク)
  • 共通化すべき要素をレイヤー(階層)で整理する考え方
  • 各コンポーネント(ボタン・入力欄・カード・モーダルなど)で何を統一すべきか
  • Design Tokens を起点にした共通化の進め方
  • 「いつ共通化すべきか」の実践的な判断ルール

コード例は React + CSS(CSS変数) で示しますが、考え方はどのフレームワークでも共通です。

なぜ共通化が必要なのか

共通化していないアプリには、次のような問題が起こりがちです。

  • デザインのブレ: ボタンの色や角丸が画面ごとに微妙に違う
  • 修正漏れ: 「ボタンの色を変えて」の一言で、何十か所も直す羽目になる
  • 保守コストの増大: 似たコードが散らばり、どれが正解か分からなくなる

共通化すると、これらが次のように改善されます。

  • 一貫性: ユーザーは「同じ見た目=同じ操作」と学習でき、使いやすくなる
  • 変更容易性: 1か所直せば全画面に反映される
  • 開発速度: 部品を組み合わせるだけで画面が作れる

一方で、「共通化しすぎる」のも危険です。まだ1か所でしか使っていないものを無理に汎用部品にすると、やたらと引数(props)が増えた「何でも屋コンポーネント」が生まれ、かえって読みにくくなります。共通化はあくまで「重複が実際に発生している、または確実に発生する」要素から始めるのがコツです。この判断軸は記事の最後で改めて触れます。

共通化を検討する要素の全体像

共通化すべき要素を「ボタン」「カード」とフラットに並べると、数が多くて混乱します。そこで本記事では、依存関係のレイヤー(階層)で整理します。レイヤーの数字が小さいほど土台で、レイヤーの数字が大きいの層はレイヤーの数字が小さい層を組み合わせて作られます。

この5レイヤーに沿って、それぞれの要素を見ていきましょう。

レイヤー1: Design Tokens(すべての土台)

Design Tokens(デザイントークン) とは、色・影・角丸・余白・フォントサイズといった**「デザインの値そのもの」に名前をつけて一元管理したもの**です。#3b82f6 のような生の値を直接書く代わりに、--color-primary のような名前で参照します。

なぜ最初にこれを決めるべきかというと、この後に作るすべての部品がここを参照するからです。トークンを1か所変えれば、ボタンもカードも入力欄も一斉に変わります。逆にトークンがないと、各部品に色や余白がバラバラに散らばり、共通化の効果が半減します。

tokens.css
: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: アイコンだけの小さな操作ボタン

「保存・キャンセル・削除」以外の補助操作もこのパターンに乗せると、ボタンの見た目が散らからずに済みます。

Button.jsx
// variant で見た目を切り替える1つのボタンに集約する
export function Button({ variant = "primary", children, ...props }) {
  return (
    <button className={`btn btn--${variant}`} {...props}>
      {children}
    </button>
  );
}
Button.css
.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.css
.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項目を、ラベル・必須表示・補足文・エラーメッセージ・入力部品でひとまとめにしたものです。これを共通化すると、フォームの見た目とアクセシビリティ(ラベルと入力の紐付け)が一気に揃います。

FormField.jsx
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: エラー時の「説明文・再試行ボタン」
EmptyState.jsx
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回で共通化」のリズムで、少しずつ整えます。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?