Motomichi Works Blog

その日学習したことについて書いている日記です。誰かの役に立ったらそれはそれで嬉しいです。

WebデザインからCSS設計について考える その0005 SMACSSとMindBEMdingを組み合わせてCSS設計をする

はじめに

SMACSSとMindBEMdingの良いとこ取りで混ぜます。

これによって長期的なメンテナンスがしやすく、担当者交代やメンバーの増員に強いCSS設計を考えていきます。

SMACSSによるカテゴリ分類を基盤として、接頭辞 + MindBEMdingのクラス命名規則を取り入れます。

この記事ではMindBEMdingとBEMは同じものとして話を進めていきます。

グローバルなクラスとローカルなクラス

グローバルなクラス

この記事では、

以下のような、単独でどこのHTML要素に適用しても効果のあるクラス.hogeをグローバルなクラスと呼ぶことにします。

.hoge{color:#ff0000;}

このような、単独で効果のあるグローバルなクラスがなるべく少なくなるようにCSSを書くことにしています。

グローバルなクラスがあまり多いと将来的に怖いなと思ってのことです。

ローカルなクラス

この記事では、

以下のような.exampleの子要素としてしか効果の無いクラス.hogeをローカルなクラスと呼ぶことにします。

.example .hoge{color:#ff0000;}

以下のような.exampleとの複合でしか効果の無いクラス.hogeについても、ローカルなクラスと呼ぶことにします。

.example.hoge{color:#ff0000;}

SMACSSとMindBEMdingを混ぜる概要

SMACSSの5つのカテゴリをベースとして全体設計をしていきます。

SMACSSのクラス命名規則である「接頭辞をつける」という考え方は残したまま、レイアウトとモジュールのクラス命名規則にはBEMを持ち込みます。

modifierおよびステートはモジュールにもレイアウトにも組み合わせることが可能で、もちろんローカルなクラスに組み合わせることも可能です。

今回のSMACSSによるカテゴリ分けと、クラス命名規則を表にすると以下のようになります。

カテゴリ 接頭辞 クラス命名規則
ベース なし なし
レイアウト l- 接頭辞 + BEM
モジュール なし BEM
ステート is- 接頭辞
テーマ theme- 接頭辞

SMACSSのモジュールについて、サブクラスを付与してサブモジュールを作成する手法がありますが、これはBEMとして置き換えるため、あくまでもSMACSSの中から使用する部分は「カテゴリ分け」と「接頭辞」だけとなります。

具体的にはSMACSSで.example .example-subと命名していたものは.example .example--subと命名されることになります。

カテゴリの詳細

具体的なCSSとHTMLを書いたりしながらカテゴリの詳細について説明します。

ベース

今回は「HTML5 Doctor CSS Reset and all others on CSSReset.com」を使用することにします。

normalize.css や sanitize.cssはもちろんのこと、bootstrapを丸ごとベースとしてみるのも良いと思います。

レイアウト

l-が接頭辞です。

例として、以下のような2種類のページレイアウトを用意してみます。
MindBEMdingの命名規則を採用しながら、なるべくグローバルなクラスを増やさないようにしています。
レイアウトカテゴリで、以下の例では 「.l-page-type-0001」 と 「.l-page-type-0002」 の二つです。

.l-page-type-0001 {}
.l-page-type-0001 .l-page-type-0001__first-area {
  display: block;
}
.l-page-type-0001 .l-page-type-0001__second-area {
  display: block;
}
.l-page-type-0001 .l-page-type-0001__third-area {
  display: block;
}
.l-page-type-0002 {}
.l-page-type-0002 .l-page-type-0002__first-area {
  display: block;
}
.l-page-type-0002 .l-page-type-0002__second-area {
  display: block;
}
.l-page-type-0002 .l-page-type-0002__second-area:after {
  content: ".";
  display: block;
  height: 0;
  clear: both;
  visibility: hidden;
  overflow: hidden;
  font-size: 0.1em;
  line-height: 0;
}
.l-page-type-0002 .l-page-type-0002__second-area-main {
  display: block;
  width: 66.66666666%;
  float: left;
}
.l-page-type-0002 .l-page-type-0002__second-area-sub {
  display: block;
  width: 33.33333333%;
  float: left;
}
.l-page-type-0002 .l-page-type-0002__third-area {
  display: block;
}

モジュール

例として以下の画像のようなモジュールのcssを具体的に書いてみます。
「1.ユーザー情報入力 → 2.入力情報確認 → 3.会員登録完了」などで使用するモジュールです。

f:id:motomichi_works:20160816162408p:plain

この例では、blockの要素に付与するmodifierや、現在ページを表現するための子要素に付与するmodifierがあります。

子要素のクラス(element)は必ずローカルなクラスとして定義していきます。

.step-info-0001{}
.step-info-0001 .step-info-0001__ul-ol-elm {
  width: 90%;
  margin-left: auto;
  margin-right: auto;
  zoom: 1;
}
.step-info-0001 .step-info-0001__ul-ol-elm:after {
  content: ".";
  display: block;
  height: 0;
  clear: both;
  visibility: hidden;
  overflow: hidden;
  font-size: 0.1em;
  line-height: 0;
}
.step-info-0001 .step-info-0001__li-elm {
  position: relative;
  padding-left: 12px;
  padding-right: 12px;
  float: left;
  box-sizing: border-box;
  text-align: center;
}
.step-info-0001 .step-info-0001__li-elm + .step-info-0001__li-elm:before {
  position: absolute;
  content: "";
  display: block;
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 8px 0 8px 12px;
  border-color: transparent transparent transparent #c9c9c9;
  top: 16px;
  left: -6px;
}
.step-info-0001 .step-info-0001__li-elm.step-info-0001__li-elm--current .step-info-0001__num {
  border: 4px solid #db8915;
  color: #db8915;
  background: #fbeeda;
}
.step-info-0001 .step-info-0001__li-elm.step-info-0001__li-elm--current .step-info-0001__text {
  color: #db8915;
}
.step-info-0001 .step-info-0001__num {
  display: block;
  width: 32px;
  height: 32px;
  line-height: 32px;
  margin-left: auto;
  margin-right: auto;
  font-size: 18px;
  font-weight: 700;
  text-align: center;
  border: 4px solid #c9c9c9;
  color: #c9c9c9;
  background: #f3f3f3;
  border-radius: 20px;
}
.step-info-0001 .step-info-0001__text {
  padding-top: 8px;
  text-align: center;
  color: #c9c9c9;
}
.step-info-0001.step-info-0001--3-column .step-info-0001__li-elm {
  width: 33.33333333%;
}
.step-info-0001.step-info-0001--4-column .step-info-0001__li-elm {
  width: 25%;
}
.step-info-0001.step-info-0001--5-column .step-info-0001__li-elm {
  width: 20%;
}

上記のCSSに対応するHTML文書構造は以下の通りです。

<div class="step-info-0001 step-info-0001--4-column">
  <div class="step-info-0001__first-wrap">
    <ul class="step-info-0001__ul-ol-elm">
      <li class="step-info-0001__li-elm step-info-0001__li-elm--current">
        <div class="step-info-0001__num">
          1
        </div>
        <span class="step-info-0001__text">
          テキスト
        </span>
      </li>
      <li class="step-info-0001__li-elm">
        <div class="step-info-0001__num">
          2
        </div>
        <span class="step-info-0001__text">
          テキスト
        </span>
      </li>
      <li class="step-info-0001__li-elm">
        <div class="step-info-0001__num">
          3
        </div>
        <span class="step-info-0001__text">
          テキスト
        </span>
      </li>
      <li class="step-info-0001__li-elm">
        <div class="step-info-0001__num">
          4
        </div>
        <span class="step-info-0001__text">
          テキスト
        </span>
      </li>
    </ul>
  </div>
</div>

ステート

is-が接頭辞です。

ステートクラス名は多くの箇所で同じクラス名が定義されるケースが考えられますが、常にローカルなクラスとして定義することとします。

タブのモジュールについて、.is-activeを定義する場合の例は以下の通りです。
is-activeクラスはJSのクリックイベントで取ったり付けたりするクラスなので、接頭辞is-命名規則のクラスです。

modifierは先ほどモジュールカテゴリのstep-info-0001で例を挙げていますが、ページ描画後にJSで取ったり付けたりしないクラスですのでこの違いに注意してください。

.tab-0001 {}
.tab-0001 .tab-0001__nav-ul-ol-elm {
  border-bottom: 1px solid #777777;
}
.tab-0001 .tab-0001__nav-ul-ol-elm:after {
  content: ".";
  display: block;
  height: 0;
  clear: both;
  visibility: hidden;
  overflow: hidden;
  font-size: 0.1em;
  line-height: 0;
}
.tab-0001 .tab-0001__nav-li-elm {
  list-style: none;
  float: left;
}
.tab-0001 .tab-0001__nav-anc-elm {
  position: relative;
  display: block;
  border: 1px solid #ffffff;
  border-bottom: 0;
  width: 160px;
  text-align: center;
  height: 40px;
  line-height: 40px;
  font-size: 16px;
  text-decoration: none;
  color: #777777;
}
.tab-0001 .tab-0001__nav-anc-elm.is-active {
  border: 1px solid #777777;
  border-bottom: 0;
}
.tab-0001 .tab-0001__nav-anc-elm.is-active:after {
  display: block;
  content: "";
  position: absolute;
  left: 0;
  bottom: -1px;
  height: 1px;
  width: 100%;
  background: #ffffff;
}
.tab-0001 .tab-0001__contents-ul-ol-elm {}
.tab-0001 .tab-0001__contents-li-elm {
  list-style: none;
  display: none;
}
.tab-0001 .tab-0001__contents-li-elm.is-active {
  display: block;
}

上記のCSSに対応するHTML文書構造は以下の通りです。

<div class="tab-0001">
  <ul class="tab-0001__nav-ul-ol-elm">
    <li class="tab-0001__nav-li-elm">
      <a class="tab-0001__nav-anc-elm" href="#">
        タブ0001
      </a>
    </li>
    <li class="tab-0001__nav-li-elm">
      <a class="tab-0001__nav-anc-elm is-active" href="#">
        タブ0002
      </a>
    </li>
    <li class="tab-0001__nav-li-elm">
      <a class="tab-0001__nav-anc-elm" href="#">
        タブ0003
      </a>
    </li>
  </ul>
  <ul class="tab-0001__contents-ul-ol-elm">
    <li class="tab-0001__contents-li-elm">
      コンテンツ0001
    </li>
    <li class="tab-0001__contents-li-elm is-active">
      コンテンツ0002
    </li>
    <li class="tab-0001__contents-li-elm">
      コンテンツ0003
    </li>
  </ul>
</div>

テーマ

theme-が接頭辞です。

body要素または、そのひとつ内側の要素に付与することを想定しています。

例えばbody要素にtheme-aquaなどを付与したときに、背景色やすべてのモジュールがそれに準じた色に変化するなどといった用途に使用します。

例として以下の通りです。同じ要領でモジュールなどにも色を適用していきます。

.theme-aqua .l-page-type-0001 {
  background:#ddddff;
}
.theme-aqua .l-page-type-0002 {
  background:#ddddff;
}

ディレクトリ構造

ここまで、通常のCSSのコードとして説明を進めてきましたが、sassなどのCSSプリプロセッサを使用している場合のディレクトリ構造の例を以下に挙げてみます。

基本的に1blockにつき1ファイル用意します。

scss/
  style.scss
  base/
    _reset.scss
  layout/
    _l-page-type-0001.scss
    _l-page-type-0002.scss
  module/
    _step-info-0001.scss
    _tab-0001.scss
  theme/
    _theme-aqua.scss

このようなカテゴリごとに分かれたディレクトリ構造や、blockごとに分かれたファイルによっても引き継ぎがしやすくなると考えています。

引き継ぎ項目について考える

新メンバーの加入や担当者の交代のときには、独自に策定したルールが無いのが理想的なので、SMACSSとBEMを混ぜるだけに留めています。

できるだけ覚えやすいように混ぜたつもりです。

SMACSS

SMACSSについて必要な部分を要約すると

WebデザインからCSS設計について考える その0003 SMACSSの必要部分について学ぶ - MOTOMICHI WORKS BLOG

に書いた内容を理解してもらうぐらいです。

今回必要となる部分は「カテゴリ分け」と「3つの接頭辞」についての理解です。

BEM(MindBEMding)

BEMについて必要な部分を要約するとblock-name__element-name--modifier-nameという命名規則を覚えてもらうぐらいです。

具体的には以下の4種類のクラスです。

種類 命名規則 具体例
blockの命名 .block-name .step-info-0001
blockに対するmodifierの命名 .block-name--modifier-name .step-info-0001--3-column
elementの命名 .block-name__element-name .step-info-0001__li-elm
elementに対する
modifierの命名
.block-name__element-name--modifier-name .step-info-0001__li-elm--current

グローバルなクラス

グローバルなクラスは 「BEMでいうところのblockにあたるクラス」 「SMACSSの"テーマ"にあたるクラス」 だけです。
これは、スタイルは必ずblock単位で適用されるという事を表しています。
そういうことなので、もちろんHTMLもblock単位で記述されますし、全てのページはblockの入れ子で表現できるということです。

この記事の例で挙げてきたクラスの場合は、グローバルなクラスは以下の5クラスだけであり、scssファイル名と対応しています。

  • レイアウトのblockクラス(今回の例では.l-page-type-0001と.l-page-type-0002)
  • モジュールのblockクラス(今回の例では.step-info-0001と.tab-0001)
  • テーマクラス(今回の例では.theme-aqua)

上記したグローバルなクラスのうち、モジュールについてはスタイルガイドを作成することによって引き継ぎがより単純になります。

モジュールの大きさ

blockが子として内包するローカルなクラスの種類は、その種類が多くても良いと考えています。

それに対して、特定のblockの中にあまり多くのblockを入れ子にするHTMLは推奨していません。

その為には、多少汎用性は犠牲にしても、blockをあまり細かく分割しない方が長期的な運用に適したCSS設計だと考えています。

詳細度は無駄に上げない

例えば以下のようなHTMLを書く場合のCSSについてです。

<div class="example">
  <p class="example__hoge">
    <span class="example__foo">
      サンプル
    </span>
  </p>
</div>

.example__fooセレクタ

.example {}
.example .example__hoge{}
.example .example__hoge .example__foo{}

と書いてしまうと、.example__foo{}セレクタがいたずらに詳細化してしまうため、以下のように書きます。

.example {}
.example .example__hoge{}
.example .example__foo{}

:first-childなどのセレクタを使用する場合は、親要素が何であるかを明示しないといけないので必然的に少し詳細度が増すことになりますが、そのようにやむを得ない場合を除いては、後者のようにすべて.exampleの子孫であるという事だけを示しておけば良く、.example__hogeの子孫であることは示す必要は無いということです。

詳細度が過剰になっていくと著しくレンダリングのパフォーマンスが落ちていきます。

クラス名に含まれる数字の前後はハイフンを入れる

冗長ですが、数字の前後にはハイフンを入れることにしています。

ローカルなクラスの命名規則

BEMに倣って.block-name__のところだけ守られていれば、element-nameは単語ごとにハイフンでつないであれば、何でも良いです。

blockの外側には影響を与えないクラス名なのであまり深く考えすぎなくても良いかと思います。

とにかく後任者が覚えることが少なくなるように考えています。

細かなCSS作成ルールは不要?

覚えることが増えると良くないので、おおよそ上記したルールぐらいに考えています。

全てのグローバルなクラスが把握しやすく管理され、スタイルガイドがきちんと更新され続けることが最も重要です。

後任者はグローバルなクラスのみに気を配って引き継ぎを受け、運用を進めていけばよいことになると思います。

スタイルガイドの作成

以下のsweet-styleguideリポジトリのproject1ディレクトリをドキュメントルートに配置するだけでスタイルガイドが簡単に作成できます。

GitHub - MotomichiWorks/sweet-styleguide

sweet-styleguideの詳細な使い方は以下の記事に書いています。

まとめ

  1. 独自のルールを定義せず、広く知られるSMACSSとMindBEMdingを組み合わせたルールで設計した。
  2. SMACSSのうち必要なのは「カテゴリ分け」「3つの接頭辞」についてだけ。
  3. BEMのうち必要なのはMindBEMdingと呼ばれる命名規則 block-name__element-name--modifier-name だけ。
  4. [1][2][3]の効果として、担当者が交代したときや増員したときにも、学習範囲はSMACSSやMindBEMdingに限られるので、引き継ぎがしやすい。
  5. 「is-」はJSのイベントで取ったり付けたりするが、「modifier」はJSのイベントで取ったり付けたりはしない。
  6. パフォーマンスや汎用性はある程度犠牲になるけど、blockごとにスコープ(CSSの編集による影響範囲)を限定する。
  7. [6]の効果として、CSSの編集はblockの外には影響しないので安心して編集できるし、管理がしやすく長期的な運用も見通せる。
  8. [6]の効果として、グローバルなクラスはblockとthemeのみであり、後任者や新しく加わったメンバーも簡単に全体把握できる。
  9. スタイルガイドを作成し、マメに更新し続けることはCSSの運用をしていく上では必須。

何年かCSSを書いてきた中での失敗も踏まえて、個人的には上記のようなことが現在可能なひとつの答えとして出てきたのでまとめてみました。

もちろん多くの方にそれぞれの書き方があり、一例としてですが、個人的にはCSS設計との闘いはこの辺りで終わりにしたい。ということです。

おまけ これまでに試行錯誤したことなど

参考までに、これまで試行錯誤したことや、選択に至った経緯などを書いておきます。

記事としてはここまでで既に成立しているので、ここから下はあくまでもおまけです。

レンダリングパフォーマンスとBEM

例えば.exampleというblockを作成する場合のCSSは以下のように二種類考えました。

.example {}
.example__hoge{color: red;}
.example__foo{color: blue;}
.example {}
.example .example__hoge{color: red;}
.example .example__foo{color: blue;}

3つのグローバルなクラスでblockが構成される前者の方がレンダリングパフォーマンスが良く、cssのファイルサイズも抑えられるのですが、後者の方法を選択しました。
長期にわたって運用が行われる場合、担当者の交代だったり、とても忙しい時期だったり、メンバーが増えたり色々なことが起こります。
そういった中で、前者の書き方の場合、.example__hogeはグローバルなクラスであるため「ちょっと文字を赤くしたい」と思ったときに
<span class="example__hoge">テキスト</span>
をどこに記述しても赤くできてしまいます。

それをできなくすることで、そういったイレギュラーな記述がHTMLの中に存在せず、必ずblock単位でスタイルが適用されることが保証されるため、無駄な確認の手間なども無く、安心してCSSの編集ができます。
そういった理由から後者の記述方法を選択して、グローバルなクラスが増えないようにしています。

引き継ぎを受ける側も安心ですよね。

blockの入れ子

ひとつの大規模なblockを構成する要素として、中規模のblockや小規模のblockをあまり入れ子にしない方法を選択しています。
このあたりは「大規模なblockを分割せずにひとつのblockとする」か「もっと分割すべき」かの議論はあると思いますが、スタイルガイドにまとめたときにいかに分かり易いかが重要であると考えています。
全く分割しないという事ではありませんが、あまり多くのblockに分割すると、大規模なblockは必ずそれらに依存してしまうためわかり難さが増します。
設計した本人はそう思わないかもしれませんが、後から加わったメンバーや、最悪の場合は前任者が退職されてからアサインされた担当者にはわかりにくくなります。
分割するのは、ボタン、フォーム関連要素、ローディングアイコンなど、ごく小規模であったり、汎用性の極めて高いblockにとどめると良いという判断をしています。
中途半端な汎用性のためにblockを分割するのは、CSSの長期的な運用の視点では危険だと考えています。

ついでになりますが、他のblockに依存しないblockは単体で別のプロジェクトに持ち込むことも容易になるという利点もあります。

キャメルケースによるクラス命名

詳細は割愛しますが、BEMを使用せずにSMACSSだけで設計しようと考えていたときに試していました。
BEMを取り入れることにした為、キャメルケースを選択する理由が無くなり止めました。

なぜSMACSS単体での設計をやめたのか

モジュール(BEMでいうところのblock)とサブクラスの関係は良いとしても、モジュールの子要素(BEMでいうところのelement)に対するmodifierにあたる役割が明確に無いので引き継ぎ時に困りそうだし、どう説明したらよいものか迷走していました。

実際にHTMLやCSSを読んでいても、.example.example-sub{}というCSSよりも.example.example--sub{}という命名規則の方がそのクラスの役割はサブクラス(modifier)であることが理解しやすいと感じたためです。

SMACSSとBEMを混ぜることに対する抵抗感があったのですが、混ぜる方が分かり易いのではないかという判断をしました。

blockの命名に悩みすぎない

block名に悩んであまりにも時間がかかるようであれば、btn-0001, btn-0002などの連番でも良いかと思います。
スタイルガイドがきちんと更新され続けることが重要だと考えています。
ただ、ひとつのプロジェクトに対して複数人でblockを頻繁に追加していく場合は、クラス名が重複しないユニークなクラス名にする方が問題が起こりませんし、あまり連番が並んでいるよりは、意味の分かるblock名がついていた方が、後から加わるメンバーにも分かり易いと思います。

グローバルな便利クラスを作らない

例えば以下のような色々な箇所に汎用的に使う「便利クラス」を定義する現場も少なくないと思いますが、これらを作りません。

  • .clearfix
  • .util-pad-top-10

上記のような「便利クラス」を作らないことにより、CSSの記述工数が増える、CSSのファイルサイズが増える、などの問題がありますが、以下のようなメリットがあると考えています。

  • 「他のグローバルなクラスに依存せず独立して機能するblock」毎にstyleガイドにまとめることができ、引き継ぎを受ける側は理解がし易くなる

引き継ぎやメンテナンスがしやすいことを優先して、このような判断になりました。

この「グローバルな便利クラスを作らない」ということにより、本記事中の 【引き継ぎ項目について考える】 の 【グローバルなクラス】 項目でも書いた通り、グローバルなクラスは 「BEMでいうところのblockにあたるクラス」 「SMACSSの"テーマ"にあたるクラス」 だけです。

また、この記事で紹介している 「.step-info-0001」 と 「.tab-0001」 のモジュールについても他のグローバルなクラスに依存しないことによって、紹介しやすいひとまとまりの単位になっており、独立性が保たれていることによる理解のしやすさや管理のしやすさが感じられるかと思います。

btnなどの小規模なblockが大規模なblock内に入れ子で配置されることによって、大規模なblockは小規模なblockの存在に依存しているとも言えますが、blockの入れ子についてはある程度許容しています。