44
35

More than 3 years have passed since last update.

コンポーネント指向をCSS設計へ取り入れよう!

Last updated at Posted at 2019-06-26

概要

この記事では FLOCSS という CSS 設計の一部を利用したシンプルな設計を紹介します。
FLOCSS を適応する程の大規模なページ構成でも無いけど、手軽にコンポーネント指向な CSS 設計を取り入れたい場合に有効だと思います。既に BEM という設計手法を知っている方は FLOCSS について知らなくても理解できる内容になっていると思います。

概要の章ではコンポーネント指向なCSS設計を行うメリットと FLOCSS について簡単に説明します。既にご存知の方は読み飛ばして頂いて結構です。

コンポーネント指向なCSS設計を行うメリット

皆さん、サイトのコーディングを進めていく中で、似た様なスタイルを複数回記述した事ありませんか?
例えば新しく B と C の UI を作るにあたって A の UI が似ているのでスタイルをコピペして手を加える等。一見、楽かも知れませんが、重複して同じスタイルを記述するのは非効率です。スタイルの重複を減らす事ができれば、その分コードの記述量も削減する事ができます。コンポーネント指向なCSS設計はそれらの課題を解決することに繋がります。

既存スタイルのコピペ例(BAD)

pattern.1(不必要な指定を消してコピペ)
/* AのUI(既存) */
.aStyle {
    width: 200px;
    height: 50px;
    float: left; /* B,C では必要ない */
    margin: 20px; /* B,C では必要ない */
    border-radius: 8px;
    background-color: red;
    overflow: hidden;
}

/* BのUI(新規) */
.bStyle {
    width: 150px;
    height: 50px;
    border-radius: 8px;
    background-color: blue;
    overflow: hidden;
}

/* CのUI(新規) */
.cStyle {
    width: 100px;
    height: 50px;
    border-radius: 8px;
    background-color: yellow;
    overflow: hidden;
}

pattern.1 では見ての通り A,B,C の UI で重複するスタイルを複数回記述しており、非効率なのが分かります。

pattern.2(不必要部分のみ上書き)
/* A(既存)のUI */
.aStyle,
.bStyle,
.cStyle {
    ...略(pattern.1と同じ)
}

/* BのUI(新規) */
.bStyle {
  width: 150px;
  float: none; /* 本来 B には必要のない指定 */
  margin: 0; /* 本来 B には必要のない指定 */
  background-color: blue;
}

/* CのUI(新規) */
.cStyle {
  width: 100px;
  float: none; /* 本来 C には必要のない指定 */
  margin: 0; /* 本来 C には必要のない指定 */
  background-color: yellow;
}

pattern.2 では同じスタイルを重複して記載しない様に A,B,C はまとめられているが、A のスタイルには B,C にとっては不要な指定が存在する為、本来 B,C には必要のない指定を A のスタイルを上書きする為に記載しており同様に非効率なのが分かります。

コンポーネント指向の例(GOOD)

汎用スタイルを使用して各UIを再現
/* 汎用(コンポーネント)スタイル */
.style {
    height: 50px;
    border-radius: 8px;
    overflow: hidden; 
}

/* AのUI */
.aParent .style {
   width: 200px;
   float: left;
   margin: 20px;
   background-color: red;
}

/* BのUI */
.bParent .style {
   width: 150px;
   background-color: blue;
}

/* CのUI */
.cParent .style {
   width: 100px;
   background-color: yellow;
}

コンポーネントとは「部品」です。スタイルで言えば UI を構築するための「部品」です。例えばボタンの UI にしても様々な大きさ、色、配置が考えられます。その「部品」として機能するには汎用的でなければいけません。汎用的なスタイル(コンポーネント)の定義は、複数存在する UI の土台として使用される事を考慮すると、共通では使用しづらいプロパティーの指定は除外します。コンポーネント指向では汎用的なスタイルと固有のスタイルで個別のUIを表現します。

FLOCSS について

基本的には BEM(MindBEMding)を拡張したような CSS 設計になります。
これまでの BEM の命名に加えて prefix(接頭辞)が付与され、さらに役割が明確になります。
FLOCSS の語源など詳しい説明は こちら(FLOCSS) を参照してください。
下記が簡単な prefix の役割説明になります。

prefix レイヤー 説明
l- Layout ページを構成するヘッダーやメインのコンテンツエリア、サイドバーやフッターといったプロジェクト共通のコンテナーブロックのスタイルを定義します
c- Component 再利用できるパターンとして、小さな単位のモジュールを定義します。
p- Project プロジェクト固有のパターンであり、いくつかのComponentとそれに該当しない要素によって構成されるものを定義します。
u- Utility ComponentとProjectレイヤーのモディファイアで解決することが難しい・適切では無い、わずかなスタイルの調整のための便利クラスなどを定義します。

前提(本題)

本稿のシンプルな CSS 設計では FLOCSS で使用される下記 prefix(接頭辞)は使用しません。

prefix レイヤー
l- Layout
p- Project

FLOCSS の Component(c-)と Utility(u-)のみを残した設計

まず、いきなりですが Project レイヤーと Component レイヤー(以下、Project と Component)は必ずしも分けなくて良いと思っています。というのも Project と Component のどちらも再利用性のあるパターンであり、その再利用される範囲が全ページなのか、プロジェクトとして定められた範囲なのかの違いです。その線引きを取っ払い、単純に再利用性のある汎用的なスタイルを Component として再定義し、各ページ固有のスタイルと差別化できていればある程度効率的にスタイルを組む事はできます。

FLOCSS では全ページ共通の Component が有り、全ページの内一範囲(カテゴリやジャンル別)毎に Project が存在するような考え方だとすれば、本稿の設計思想は、全ページ共通の Component というものは必要無く、本来 Project となる範囲毎に Component を Project の代わりとして据え置くような考え方となります。簡単に言えば Component に Project を統合するという事です。メリットとしては Component と Project の粒度や責務の切り分けで悩む必要が無くなります。Utility については深く触れませんが、補助として使用する程度であれば利便性もあるので取り入れておきます。

Component の粒度について

CSS 設計を適応する規模によります。
規模(ページ数や範囲)が大きいほど粒度は小さい方が使い回しやすいです。
ぼんやりとした抽象的な規模例ですが、粒度としては下記のようになるのでは無いかと考えます。

例1)同一のカテゴリ(サービス)に属するページ全てに設計を適応した場合の粒度

<!-- どのページでも汎用的に使用できるよう細かく Component が細分化されている -->
<div class="c-miniBox">
    <p class="c-normalText">名前</p>
    <div class="c-button c-button--flat">
        <span class="c-button__text">投票</span>
    </div>
</div>

例2)同一の目的やテーマを持っている似通った複数ページに設計を適応した場合の粒度

<!-- 複数のページレイアウトは似ており、UI も割と共通なため Component の粒度が大きい -->
<div class="c-miniBox">
    <p class="c-miniBox__name">名前</p>
    <div class="c-miniBox__button">
        <span class="c-miniBox__buttonText">投票</span>
    </div>
</div>

コーディングルールについて

コンポーネント指向を元にコーディングを行っていく上で取り決めたルールを紹介します。

用語説明

用語 説明
Component BEM の B(Block)に接頭辞 "c-" がついたものを表しています。
component.css Component のスタイルの記載する css であり、全ページで共通で読み込む css です。
page.css そのページ固有のスタイルを記載する css であり、各々のページで読み込む css です。

1. Component はなるべく汎用性を意識して命名する

<!-- BAD -->
<div class="c-superHeroDesc">
    <dl class="c-superHeroDesc__list">
        <div class="c-superHeroDesc__group">
            ...
        </div>
    </dl>
</div>
<!-- GOOD -->
<div class="c-packageDesc">
    <dl class="c-packageDesc__list">
        <div class="c-packageDesc__group">
            ...
        </div>
    </dl>
</div>

Component は複数のページでの使用、繰り返し使われる想定であるのが前提です。
BAD な例の命名では「スーパーヒーローの説明」という命名ですので、「スーパーヒーロー」以外の情報が入ってきた時に命名と中身の情報が全く合わないものになってしまいます。割とどんな情報が入ってきても良いように抽象度を高めた命名にしましょう。

2. Component には直接 margin / width を付与しない

component.css(複数ページ共通で使用するcss)
.c-hoge {
    display: flex;
    font-size: 14px;
    padding: 16px;
    color: #333;
    /* width: 800px; 付与しない */
    /* margin-bottom: 30px; 付与しない */
}
index.html
<div class="c-hoge">
    <div class="c-hoge__imgWrap">
        <img class="c-hoge__img" src="" alt="">
    </div>
    <ul class="c-hoge__items">
        <li class="c-hoge__item">アイテム1</li>
        <li class="c-hoge__item">アイテム2</li>
        <li class="c-hoge__item">アイテム3</li>
    </ul>
</div>

Component に予め margin や width などのスタイルが固定値で付与されていると汎用的に使い回しづらくなります。margin が付与されていると Component のスタイルを html へ適応した際、それは何らかの要素との間に意図しない余白を生じさせます。それは事実上 Component によって Component 外へ影響を与えてしまっています。

width に関しては margin ほど Component 自身が外へ与える影響は少ないですが、例えば親要素が display:flex だった場合や、要素自身を display:inline-block に変えた場合、 width の指定を打ち消す必要な場合もあります。また width:auto (width 記載無し)にしておけば div, ul, p などの要素は、その外側の要素の幅に依存するので Component 側で余計に width を設定せずに済む場合があります。

3. Component への margin / width 付与は Component では無い親要素経由で付与するか、マルチクラスを利用する

page.css(そのページ自身で使用するcss)
/* ① 親要素(.content)を経由して margin/width 付与 */
.content .c-hoge {
    width: 800px;
    margin-bottom: 30px;
}
/* ② マルチクラス指定(別のclass名)による margin/width 付与 */
.detail {
    width: 800px;
    margin-bottom: 30px;
}
index.html
<!-- ① 親要素を経由して margin/width 付与 -->
<section class="content">
    <h1 class="content__title">Hoge Component</h1>
    <div class="c-hoge">
        ...
    </div>
</section>

<!-- ② マルチクラス指定による margin/width 付与 -->
<div class="c-hoge detail">
    ...
</div>

直接 Component に margin/width を付与できないとすれば、Component 間の margin 設定ならびに width はどうすれば良いでしょう。ページレイアウトを組むには margin は必須になります。

解決策として Component 要素では無い親要素を経由して Component にスタイルを付与するか、マルチクラス指定により別の class 名としてスタイルを付与します。component.css(Component スタイル) は複数のページで共通して使用されるため、margin を直接付与してしまうと複数ページ各々で margin 調整したいのに、最初から余計な margin がついてしまいます。 page.css(ページ固有スタイル)で Component では無い親要素経由での指定、またはマルチクラス指定で Component に margin を設定することで、そのページに適した margin を Component に設定することができます。

4. 特定のページで Component や、その内部に変更を加えたい場合は Component を直接変更しない

component.css
/* 変更しない */
.c-hoge {
    padding: 16px;
}
.c-hoge__imgWrap {
    margin-bottom: 16px;
}
.c-hoge__item {
    background-color: #ddd;
}
page.css
/* Component のスタイルを上書き */
.content .c-hoge {
    padding: 8px;
    border: 1px solid #ddd;
}
.content .c-hoge__imgWrap {
    margin-bottom: 8px;
    border-radius: 8px;
}
.special {
    font-weight: bold;
    background-color: #ffd700;
}

index.html
<section class="content">
    <h1 class="content__title">Hoge Component</h1>
    <div class="c-hoge">
        <div class="c-hoge__imgWrap">
            <img class="c-hoge__img" src="" alt="">
        </div>
        <ul class="c-hoge__items">
            <li class="c-hoge__item">アイテム1</li>
            <li class="c-hoge__item special">アイテム2</li>
            <li class="c-hoge__item">アイテム3</li>
        </ul>
    </div>
</section>

Component(component.css) は複数ページ共通スタイルなので、あるページで少し見た目を変更したいがために変更を加えてしまうと他のページで同じ Component が使用されていた場合に影響を及ぼしてしまいます。Component に margin/width を付与する時と同じように page.css(ページ固有スタイル)で Component では無い親要素経由での指定、またはマルチクラス指定を利用してスタイルを上書きましょう。

親要素経由かマルチクラス指定どちらでスタイル付与すべきか

親要素経由とマルチクラス指定によるスタイル付与では基本前者をお勧めします。理由として、都度 Component にマルチクラスを指定するのは結構面倒(運用コストが大きい)です。また html 側の可読性も悪くなってしまいます。ただ、親要素経由での指定にも欠点があります。
固有のスタイルを付与したい Component 要素が複数同一の class 名として同じ階層に並んでいた場合、親要素経由でスタイル指定する際 :nth-child(n)等を使用する必要が出てきます。:nth-child(n)による実装だと要素の順番が入れ替わった際に意図しない要素にスタイルが当たってしまう等の問題があるので、このような場合にはマルチクラス指定によるスタイルの付与が望ましいです。

5. Section 要素は Component として扱わない

index.html(BADなComponent例)
<!-- 背景色(透明)が見た目上青色に染まってしまう -->

<section class="c-fuga"> <!-- 背景色(青) -->
    <h1 class="c-fuga__title">見出し</h1>
    <div class="c-fuga__content">
        ...
    </div>
    <section class="c-piyo"> <!-- 背景色(透明)-->
         <h1 class="c-piyo__title">小見出し</h1>
         ...
    </section>
</section>
index.html(GOODなComponent例)
<!-- 背景色(青)と(透明)が見た目上別れつつ理想のアウトラインを保っている -->

<section class="content">
    <div class="c-fuga"> <!-- 背景色(青) -->
        <h1 class="c-fuga__title">見出し</h1>
        <div class="c-fuga__content">
            ...
        </div>
    </div>
    <section class="content content--sub">
        <div class="c-piyo"> <!-- 背景色(透明)-->
            <h1 class="c-piyo__title">小見出し</h1>
            ...
         </div>
    </section>
</section>

アウトライン(文章の階層構造・見出し階層)は作成するページごとに柔軟に決定することができる状態が望ましい。また、コンポーネントごとに背景色を持っている場合、Component が section 要素になっていると場合によってはスタイルの再現が難しくなってしまう。 具体的には、ページのアウトラインを構成する上で、「見出しA」に対して「小見出しA」を作りたい場合、「見出しA」の section の中に「小見出しA」の section をネストさせる必要が出てくる。この時に Component(section) に背景色を指定していると、お互い違う背景色を持った枠(Component)として分けて表示するといったスタイルの実現は難しくなる。

6. font-size や color などの親要素から引き継がれるスタイルは、なるべく親要素に記載する

/* BAD */
.c-fuga {
    text-align: center;
    background-color: #999;
}
.c-fuga__text {
    color: #fff;
    font-size: 14px;  
}
.c-fuga:hover {
    background-color: #333;
}
.c-fuga:hover .c-fuga__text {
    color: #ff0;
}
/* GOOD */
.c-fuga {
    color: #fff;
    font-size: 14px;  
    text-align: center;
    background-color: #999;
}
.c-fuga:hover {
    color: #ff0;
    background-color: #333;
}

これは正直 Component の見た目にもよります。子要素でなく親要素にスタイルを設定しても問題無いような場合、なるべく親要素にスタイルを記載するのが良いと自分は考えます。

例えば要素を hover した際にテキストの色を変更する事は良くあります。多くの場合はそのテキストを hover した時よりも、そのテキストを含む要素を hover した際に色を変更する場合が多いかと思います。このような場合、子要素(テキスト)に直接 color を設定していると BAD な例のように hover 時のスタイル指定が別れてしまう場合があります。GOOD 例のように親要素に color を設定しておけば、それを引き継ぐ子要素も hover した際に同じ色に変化してくれます。

また、Component に margin を付与する際にまとめて font-size, color, text-align などの変更も親要素だけで済む場合があるので、この点がメリットになります。

7. Utility は適度に利用

.u-red {
    color: #f00 !important;
}
.u-link {
    color: #00f !important;
    text-decoration: underline !important;
}
<div class="caution">
    <p class="caution__text">
        明日の天気が<span class="u-red">晴れ</span>であればイベントを開催します。
        <br><span class="u-red"></span>であればイベントは中止になります。<a href=""
        class="u-link">詳しくはこちらをご覧ください。</a>
    </p>
</div>

例えば ○○ のテキストの色を赤色にして目立たせてくれ!だったり、ここはリンクにしてほしい、入れてほしいといった事は良くある事かと思います。こう言った軽微な変更を予め Component に含めておくのは面倒だし、含めておいても使用されない場合があります。このような時にいくつか Utility 化しておくと柔軟に変更でき利便性があります。Utility は必ずここは 〇〇 なスタイルに変更したい!といった微調整に使用するので、!important を指定しておいても良いと思います。

最後に(余談)

FLOCSS でいう Project と Component の切り分けは Atomic Design でいう Atom, Molecule, Organisim の切り分けと似通っている部分があると思います。Atomic Design はサイトの規模が大きいほど Atom, Molecule, Organisim といった粒度の切り分けに恩恵を感じる部分が出てくると思いますが、やはりそこまで規模が大きくない場合は恩恵を感じづらく、ただ運用コストが高いだけになりがちです。実際弊社のとあるチームでも最初 Atomic Design を導入していたが、現在は汎用性のあるコンポーネントとそうでないモノといった独自のシンプルな切り分けに移行していたりします。ただ既存設計を当てはめるのでは無く、規模の大きさによって適切な粒度の切り分けは考える必要があり、FLOCSS も同じように例外ではないと思っています。

44
35
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
44
35