ブログ

CSS設計におけるコンポーネントのスタイルを上書きする方法を考える

CSS設計を考える上で、「使用する場所に応じてスタイルの一部を変更したいコンポーネント」の扱いをどうするべきかは大きなテーマの一つだと感じます。

方法は一つではないため、そのプロジェクトにおける最適な方法を選び、プロジェクト全体を通して一貫した書き方を維持することが重要だと考えます。この記事では4つの方法について考察しています。

モディファイアでスタイルを上書きする方法

一般的にはよく推奨されている方法です。コンポーネントにモディファイアを付けて、そのモディファイに応じてスタイルを変更するパターンです。

以下の例では、c-buttonの基本スタイルに対して、c-button--reversec-button--icon-smallといったモディファイアを追加してスタイルを上書きしています。

index.html
<div class="p-about__button">
<a href="./about/" class="c-button c-button--reverse c-button--icon-small">
<span class="c-button__text">もっと見る</span>
<span class="c-button__icon"><img src="./img/common/icon_arrow.svg" alt=""></span>
</a>
</div>
_c-button.scss
.c-button {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
column-gap: 10px;
}
.c-button__icon {
width: 25px;
}
// c-button--reverseに関するスタイル
.c-button--reverse {
grid-template-columns: auto minmax(0, 1fr); // grid-template-columnsを上書き
}
.c-button--reverse > .c-button__icon {
order: -1; // orderを追加
}
// c-button--icon-smallに関するスタイル
.c-button--icon-small > .c-button__icon {
width: 15px; // widthを上書き
}

この方法のメリットとデメリットは以下の通りです。

メリットデメリット
  • よく使うスタイルをモディファイアで定義することで、複数箇所で同じコードを繰り返し記述する必要がなくなる。再利用性が高い
  • モディファイア名から、上書きされているスタイルの意図が直感的に理解しやすい。
  • 親要素に依存せず、コンポーネント自体に対してスタイル変更を適用できるため、コンポーネントの独立性を保てる。
  • 適切なモディファイア名を考える必要があり、命名ルールを明確にする必要がある。
  • HTML内で複数のモディファイアを組み合わせると、クラス名が長くなり、HTMLの可読性が落ちる場合がある。

「再利用性」「スタイルの意図」「コンポーネントの独立性」が大きなメリットだと感じます。特に再利用性を考慮する場合に、モディファイアでの上書きが適しており真っ先に検討すべき方法だと考えます。

一方で「微妙に異なる共通パーツが無数に存在するデザイン」ではモディファイア地獄となり、管理が大変になります。そのような場合には親要素からカスケーディングさせる方法が選択肢となるでしょう。

親要素からカスケーディングさせる方法

親要素からスタイルをカスケーディングさせる方法では、先述した「再利用性」「スタイルの意図の明確さ」「コンポーネントの独立性」といった利点の多くが損なわれます。

それでも、この方法には明確なメリットがあり、プロジェクトによっては有効な選択肢となります。

最大のメリットは、柔軟なデザインに対応できる点です。親要素の数だけスタイルを上書きできるため、「コンポーネント設計が比較的緩めなデザインデータ」でも問題なく対応可能です。以下の3つの方法について詳しく説明します。

  • 親要素のスタイルで詳細度を高める方法
  • コンポーネントに親要素固有のクラスを追加する方法
  • カスタムプロパティでスタイルを継承させる方法

①親要素のスタイルで詳細度を高める方法

親要素のクラスを頼りにコンポーネントのスタイルを上書きする方法です。

以下の例では親要素であるp-about__buttonから子要素のコンポーネントであるc-buttonのスタイルを変更しています。

index.html
<div class="p-about__button">
<a href="./about/" class="c-button">
<span class="c-button__text">もっと見る</span>
<span class="c-button__icon"><img src="./img/common/icon_arrow.svg" alt=""></span>
</a>
</div>
_c-button.scss
.c-button {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
column-gap: 10px;
}
.c-button__icon {
width: 25px;
}
.p-about__button > .c-button {
grid-template-columns: auto minmax(0, 1fr); // grid-template-columnsを上書き
}
.p-about__card > .c-button .c-button__icon {
order: -1; // orderを追加
width: 15px; // widthを上書き
}

この方法のメリットとデメリットは以下の通りです。

メリットデメリット
  • 追加のクラス名が不要なため、HTMLをシンプルに保てる。
  • 詳細度が高くなりやすく、CSSの優先順位が複雑になる場合がある。
  • 親要素とコンポーネントのスタイルが同一のファイル内に混在し、管理が難しくなる。

詳細度を低く保ちたい場合には避けたい方法です。

また.scssでファイル分割をしている場合には、上書きしたスタイルをコンポーネント側のファイルに記述するか、親要素側のファイルに記述するかをあらかじめ決めておく必要があるでしょう。

②コンポーネントに親要素固有のクラスを追加する方法

コンポーネントの各要素に親要素固有のクラスを名を併記する方法です。以下のコードではp-about__button-linkp-about__button-textp-about__button-iconを追加しています。

index.html
<div class="p-about__button">
<a href="./about/" class="p-about__button-link c-button">
<span class="p-about__button-text c-button__text">もっと見る</span>
<span class="p-about__button-icon c-button__icon"><img src="./img/common/icon_arrow.svg" alt=""></span>
</a>
</div>
_c-button.scss
.c-button {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
column-gap: 10px;
}
.c-button__icon {
width: 25px;
}
_p-about.scss
.p-about__button-link {
grid-template-columns: auto minmax(0, 1fr); // grid-template-columnsを上書き
}
.p-about__button-icon {
order: -1; // orderを追加
width: 15px; // widthを上書き
}

この方法のメリットとデメリットは以下の通りです。

メリットデメリット
  • CSSの詳細度が高くならず、優先順位の競合が発生しにくい。
  • .scssでのファイル分割時にコードの記述場所を議論する必要がない。
  • クラス名が複数となるため、HTMLが複雑に見える場合がある。
  • 上書きのスタイルを意図的に適用するには、CSSの読み込み順を適切に管理する必要がある。

詳細度を同じ状態で保てる一方、CSSの読み込み順を意識する必要があります。上記の例では.c-buttonよりも後に.p-about__button-linkのスタイルを読み込んでおく必要があります。

上書きは親要素のクラスに対して行うので、ファイル分割をしている場合でもコードの記述場所で迷うことはなくなるでしょう。

1つ前の「親要素のスタイルで詳細度を高める方法」のデメリットをかなり改善できたと言えるでしょう。

③カスタムプロパティで値を継承させる方法

先述の①と②のミックスのような方法です。

コンポーネントのルート要素に親要素由来のクラスを追加し、カスタムプロパティを使用してルート要素から子要素にスタイル適用する方法です。

以下の例ではp-about__button-linkクラスを追加しています。

index.html
<div class="p-about__button">
<a href="./about/" class="p-about__button-link c-button">
<span class="c-button__text">もっと見る</span>
<span class="c-button__icon"><img src="./img/common/icon_arrow.svg" alt=""></span>
</a>
</div>
_c-button.scss
.c-button {
--_cols: minmax(0, 1fr) auto;
--_icon-order: initial;
--_icon-size: 25px;
display: grid;
grid-template-columns: var(--_cols);
column-gap: 10px;
}
.c-button__icon {
order: var(--_icon-order);
width: var(--_icon-size);
}
_p-about_.scss
.p-about__button-link {
--_cols: auto minmax(0, 1fr); // grid-template-columnsを上書き
--_icon-order: -1; // アイコンのorderを上書き
--_icon-size: 15px; // アイコンのwidthを上書き
}

この方法のメリットとデメリットは以下の通りです。

メリットデメリット
  • 追加するクラス名が1つで済み、HTMLをシンプルに保つことができる。
  • 変更可能なプロパティをCSS内で定義するため、どの部分がカスタマイズ可能なのかが明確になる。
  • コンポーネントのスタイル設計が複雑になり、コードの読みやすさや保守性が低下する。

JavaScriptでの関数に引数を渡す方法に近いかもしれません。

コンポーネント内で宣言するカスタムプロパティが10個も20個もあると大変ですが、4〜5個程度に収まるようであればコードの読みやすさも特に気にならないと感じます。

まとめ

「再利用性」「スタイルの意図」「独立性」を保つ必要があるコンポーネントの場合は、モディファイアでの対応が第一選択となります。

実務でよくある「コンポーネント設計が比較的緩めなデザインデータ」の場合には、「親要素からカスケーディングさせる方法」のいずれかを選択する必要があります。

個人的には基本モディファイアで対応し、パターンが多いコンポーネントでは「親要素からカスケーディングさせる方法」を選択するのがベターかなと感じております(「パターンが多いコンポーネント」をコンポーネント化する必要があるのかは議論の余地あり)。

その他にもユーティリティクラスで対応する方法などもありますが今回は割愛させていただきます。

冒頭でも述べましたが、CSS設計に正解はなく、そのプロジェクトにおける最適な方法を選び、プロジェクト全体を通して一貫した書き方を維持することが何よりも重要だと考えています。

参考サイト