本文へスキップ
スキルアップカレッジ

コンポーネント指向——部品として設計する

レッスン5:コンポーネント指向——部品として設計する

このレッスンで学ぶこと

  • 「部品は置かれる場所を知らない」という原則を理解する
  • 余白を外側が持つか内側が持つかという判断を身につける
  • 通常・押下・無効といった状態の表現方法を理解する
  • ボタン・カード・フォームを部品として組む考え方を身につける

前回のレッスンまでで、詳細度とカスケードの整理、役割にもとづく命名、値の一元管理、Grid による配置という土台が揃いました。ここからは、それらを組み合わせて「部品」として設計する考え方に進みます。ここが、本コースの後半の中心テーマです。

部品は置かれる場所を知らない

コンポーネント指向という考え方の核心は、1 つの短い原則に集約できます。部品は、自分がページのどこに置かれるかを知らないほど、再利用しやすくなるという原則です。

この原則がなぜ大切なのか、うまくいかない例から見てみましょう。

.sidebar .card {
  margin-left: 20px;
}

.promo-section .card {
  margin-top: 40px;
}

.card というカードの部品が、自分の置かれる場所(サイドバーの中か、プロモーションセクションの中か)によって、外側の余白の指定が変わってしまっています。この書き方だと、カードを新しい場所に置くたびに、その場所専用の CSS を追加する必要が出てきます。カードという部品自体は 1 つのはずなのに、置く場所の数だけスタイルの分岐が増えていきます。

中核メッセージ 5:部品は、置かれる場所を知らないほど再利用できます。

.card {
  border: 1px solid var(--color-border, #ddd);
  border-radius: 4px;
  padding: 16px;
}
<div class="layout">
  <aside class="layout__sidebar">
    <div class="card">サイドバーのカード</div>
  </aside>
  <section class="layout__promo">
    <div class="card">プロモーションのカード</div>
  </section>
</div>

こちらの .card は、自分がサイドバーに置かれるかプロモーションセクションに置かれるかを一切気にしていません。カードとしての見た目(枠線・角丸・内側の余白)だけを担当し、どこに置かれても同じ見た目で表示されます。置かれる場所ごとの間隔の違いは、部品の外側、つまり親要素の側で調整します。

flowchart LR
  A[レイアウト側<br/>置く場所と間隔を決める] --> B[部品<br/>見た目と内部の余白だけを持つ]
  B --> C[どこに置いても同じ見た目]

この図は、レイアウト(置き場所を決める側)と部品(見た目を決める側)の役割分担を表しています。部品自身は、自分がどこに置かれるかという情報を一切持たず、レイアウト側がその都度、間隔を調整する構造になっています。

余白を外側が持つか、内側が持つか

部品を設計するとき、必ず突き当たる判断があります。「この余白は、部品の外側(margin)に持たせるべきか、内側(padding)に持たせるべきか」という判断です。

判断の目安はシンプルです。部品自身の見た目を保つための余白は内側に、部品どうしの間隔は外側にという切り分けです。

.card {
  padding: 16px; /* カードの中身と枠線の間の余白:部品自身の見た目 */
}

.card-list {
  display: flex;
  flex-direction: column;
  gap: 16px; /* カードどうしの間隔:部品を並べる側の責任 */
}

カードの中身が枠線にくっつかないようにする余白は、カードという部品自身の見た目の一部なので padding で内側に持たせます。一方、カードとカードの間隔は、カードを並べる側(.card-list)の責任なので、gap を使って並べる側に持たせます。

もし .card 自身に margin-bottom: 16px を指定してしまうと、カードが 1 枚だけ単独で置かれた場面でも余計な余白が付いてしまい、「部品は置かれる場所を知らない」という原則から外れてしまいます。

💡 ポイント 「この部品が単独で置かれても不自然に見えないか」を基準にすると判断しやすくなります。単独で置いても不自然でない余白は内側に、複数並んだときにだけ必要になる間隔は外側(並べる側)に置くという考え方です。

状態の表現——通常・押下・無効

部品には、見た目のバリエーションだけでなく「状態」もあります。ボタンを例にすると、通常の状態、押されている状態、操作できない無効な状態などです。

状態を表す方法として、レッスン2 で扱った Modifier をそのまま応用する方法があります。

.button {
  padding: 10px 20px;
  border-radius: 4px;
  background: var(--color-brand, #2c6cd4);
  color: #ffffff;
  border: none;
}

.button:disabled {
  background: #cccccc;
  color: #888888;
  cursor: not-allowed;
}

.button:hover:not(:disabled) {
  background: #1a4fa0;
}

:disabled や :hover のような疑似クラスは、ブラウザが自動的に判定してくれる状態です。無効な <button disabled> には自動的に :disabled が適用され、個別にクラスを付け外しする必要がありません。

一方、ブラウザが自動判定してくれない状態(選択されている、開いている、読み込み中である、など)は、状態を表すクラスを JavaScript から付け外しして表現します。このとき、見た目のバリエーションを表す Modifier(--)とは区別して、状態専用のクラスには is- という接頭辞を付ける書き方がよく使われます。

.accordion__panel {
  display: none;
}

.accordion__panel.is-open {
  display: block;
}
<div class="accordion__panel is-open">開いているときだけ表示される中身</div>

.is-open は「見た目のバリエーション」ではなく「いまの状態」を表しています。-- の Modifier が「常に固定されたバリエーション」を表すのに対し、is- のクラスは「JavaScript によって付いたり外れたりする、一時的な状態」を表すという役割分担です。この区別を付けておくと、CSS を読んだときに「これはデザインの選択肢か、それとも動的に切り替わる状態か」を見分けやすくなります。

📝 補足 色だけで状態の違いを表すと、色の判別が難しい人にとって伝わりにくくなる場合があります。無効な状態にはカーソルの形(cursor: not-allowed)を添えるなど、色以外の手がかりも一緒に用意しておくと安心です。

ボタンを部品として組む

ここまでの考え方を踏まえて、ボタンを部品として組み立ててみます。

.button {
  display: inline-flex;
  align-items: center;
  padding: 10px 20px;
  border-radius: 4px;
  border: none;
  font-size: 16px;
  cursor: pointer;
}

.button--primary {
  background: var(--color-brand, #2c6cd4);
  color: #ffffff;
}

.button--secondary {
  background: transparent;
  color: var(--color-brand, #2c6cd4);
  border: 1px solid var(--color-brand, #2c6cd4);
}

.button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
<button class="button button--primary">保存する</button>
<button class="button button--secondary">キャンセル</button>
<button class="button button--primary" disabled>送信中</button>

.button が共通の骨格(余白・角丸・文字サイズ)を担当し、.button--primary と .button--secondary がバリエーションを、:disabled が状態を担当しています。役割ごとに担当が分担されているため、新しいバリエーション(.button--danger など)を追加しても、既存のボタンの見た目には影響しません。

カードを部品として組む

カードも同じ考え方で組み立てます。

.card {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 16px;
  border: 1px solid var(--color-border, #ddd);
  border-radius: 4px;
}

.card__title {
  font-size: 18px;
  font-weight: bold;
}

.card__body {
  color: var(--color-text, #333333);
}

.card__footer {
  display: flex;
  justify-content: flex-end;
}
<div class="card">
  <h3 class="card__title">商品名</h3>
  <p class="card__body">商品の説明文がここに入ります。</p>
  <div class="card__footer">
    <button class="button button--primary">カートに入れる</button>
  </div>
</div>

.card の中に .button という別の部品がそのまま組み込まれています。部品どうしが「自分が何であるか」を知っているだけで、「自分がどの部品の中に置かれるか」を知らずに済んでいる点に注目してください。.button は .card の中にあることを一切前提にしていないため、.card の外に単独で置いても問題なく動作します。

フォームを部品として組む

フォームの部品も、同じ発想で組み立てられます。

.form-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 16px;
}

.form-field__label {
  font-size: 14px;
  font-weight: bold;
}

.form-field__input {
  padding: 8px;
  border: 1px solid var(--color-border, #ddd);
  border-radius: 4px;
}

.form-field__input:focus {
  border-color: var(--color-brand, #2c6cd4);
  outline: none;
}

.form-field--error .form-field__input {
  border-color: #e74c3c;
}

.form-field__error-message {
  font-size: 13px;
  color: #e74c3c;
}
<div class="form-field form-field--error">
  <label class="form-field__label" for="email">メールアドレス</label>
  <input class="form-field__input" id="email" type="email">
  <p class="form-field__error-message">メールアドレスの形式が正しくありません。</p>
</div>

.form-field という 1 つの Block の中に、ラベル・入力欄・エラーメッセージという Element が収まっています。.form-field--error という Modifier が付いたときだけ、内部の入力欄の枠線の色が変わる仕組みです。フォーム全体を 1 つの巨大な CSS の塊として書くのではなく、「1 つの入力項目」という単位を部品として切り出すことで、同じ構造を持つ別の入力項目にもそのまま使い回せます。

⚠️ 注意 ここで組んだフォームの部品は、あくまで見た目の構造です。入力内容の検証(メールアドレスの形式が正しいかどうかの判定など)は CSS の範囲を超えるため、本コースでは扱いません。

講師の現場メモ①——8,000 行に膨れたスタイルシートの記憶

私が最初に部品という考え方の大切さを痛感したのは、うまくいった経験からではありませんでした。ある受託案件で、1 つの CSS ファイルが 8,000 行にまで膨れ上がった状態を引き継いだときのことです。

そのファイルには、ページごとの例外指定が数えきれないほど積み重なっていました。.card という名前のクラスが 20 か所以上で使われているのに、そのうちの何か所が独自の margin を上書きしているのか、誰も把握していませんでした。ある日、フッター付近のカードの余白を調整しようとして 1 行書き換えたところ、まったく別のページの一覧表示が崩れました。原因は、そのカードが「置かれる場所を前提にした余白」を内部に抱えていたことでした。

このとき学んだのは、部品が壊れやすくなるのは、部品自身の見た目の指定と、置かれる場所に依存した指定が、同じセレクタの中に混在してしまうことが原因だということです。8,000 行のうち、実際にカードという部品の見た目を定義していたのはごく一部で、残りの大半は「このページのこの場所だから」という理由だけで積み重なった上書きでした。

この経験のあと、私は部品の CSS を書くときに、必ず「これは部品自身の見た目か、それとも置かれる場所の都合か」を自問する習慣を付けました。この 1 つの問いが、本レッスンで扱った「余白を外側が持つか内側が持つか」という判断の出発点になっています。

実習:置かれる場所に依存したカードを書き直す

次の CSS は、置かれる場所によって見た目が変わってしまうカードです。「部品は置かれる場所を知らない」という原則に沿って書き直してみましょう。

.article-list .card {
  margin-bottom: 24px;
  padding: 20px;
}

.related-list .card {
  margin-bottom: 12px;
  padding: 12px;
}

書き直しの手順は次のとおりです。

  1. .card 自身が持つべき見た目(内側の余白・枠線・角丸など)を洗い出し、.card のスタイルとして定義する
  2. 場所ごとに違っていた margin-bottom を .card から取り除く
  3. カードを並べる側(.article-list、.related-list)に gap を指定し、間隔の責任を並べる側に移す
  4. 場所によってカードの内側の余白まで変える必要がある場合は、.card--compact のような Modifier を使って明示的なバリエーションとして表現する
  5. 書き直したカードを、まったく新しい場所に単独で置いても不自然に見えないかを確認する

まとめ

このレッスンでは、以下のことを学びました。

  • 部品は、自分が置かれる場所を知らないほど再利用しやすくなること
  • 部品自身の見た目のための余白は内側(padding)に、部品どうしの間隔は外側(並べる側の gap など)に持たせること
  • ブラウザが自動判定する状態は疑似クラスで、JavaScript が付け外しする状態は is- のようなクラスで表現できること
  • ボタン・カード・フォームは、いずれも「共通の骨格・バリエーション・状態」という 3 層で組み立てられること
  • 部品の設計を誤ると、一見無関係な場所の見た目が連鎖して崩れる事故につながること

次のレッスンでは、ここまでとは対照的な設計思想である「ユーティリティファースト」という選択肢を取り上げます。


確認クイズ

このレッスンの理解度をチェックしましょう。