みなさんは CSS 設計をするとき、どの設計方針を採用してますか?
自分も SMACSS、BEM、FLOCSS と渡り歩いて来ましたが、どうにもしっくり来ない点が Modifier の記述ルールです。
ここでは自分の試行錯誤の過程と結果を公開してみました。
BEM 記法
クラス名に構造情報を持たせることで、要素のモジュール化を強要して定義の破綻を防ぐ、シンプルかつ非常に強力なルールなのですが、下記の例のように HTML 側のクラス記述が冗長になるのがデメリットです。
.local-menu { … } /* Block */
.local-menu--category { … } /* Modifier */
.local-menu__title { … } /* Element */
.local-menu__list { … } /* Element */
.local-menu__list--category { … } /* Modifier */
.local-menu__list__item { … } /* Element */
.local-menu__list__item--typeA { … } /* Modifier */
.local-menu__list__item--typeB { … } /* Modifier */
.local-menu__list__item--category { … } /* Modifier */
.local-menu__list__item--active { … } /* Modifier */
<nav class="local-menu">
<h2 class="local-menu__title">メニュー見出し(Basis)</h2>
<ul class="local-menu__list">
<li class="local-menu__list__item local-menu__list__item--typeA local-menu__list__item--active"><a href="#">メニューA</a></li>
<li class="local-menu__list__item local-menu__list__item--typeB"><a href="#">メニューB</a></li>
</ul>
</nav>
<nav class="local-menu local-menu--category">
<h2 class="local-menu__title">メニュー見出し(Modifier)</h2>
<ul class="local-menu__list local-menu__list--category">
<li class="local-menu__list__item local-menu__list__item--typeA local-menu__list__item--category local-menu__list__item--active"><a href="#">メニューA</a></li>
<li class="local-menu__list__item local-menu__list__item--typeB local-menu__list__item--category"><a href="#">メニューB</a></li>
</ul>
</nav>
CSS を見ると構成は非常に分かりやすいのですが、ネストが長くなった Element からの Modifier ともなると HTML のクラス記述が一気に膨れ上がってしまい、HTML ソースの class の箇所は何かもう見るもの嫌になってきます。目が滑ってしまい、どうにも CSS の構成が頭に入ってきません。
SMACSS、FLOCCSS の識別子記法
SMACSS、FLOCCSS ではクラス名に以下のようなプレフィックスを付けて意味を持たせる手法を採用しています。
.l-*
: レイアウト構造を示すクラス
.is-*
: 要素の状態を示すクラス
.c-*
: コンポーネント、モジュールを示すクラス
.p-*
: プロジェクト固有の要素を示すクラス
.u-*
: ユーティリティ、ちょっとしたスタイル調整をするクラス
といった意味があります。
こちらの記法で先ほどの記述を書き直してみます。
.c-local-menu { … } /* Block */
.c-local-menu--category { … } /* Modifier */
.c-local-menu__title { … } /* Element */
.c-local-menu__list { … } /* Element */
.c-local-menu__list--category { … } /* Modifier */
.c-local-menu__list__item { … } /* Element */
.c-local-menu__list__item--typeA { … } /* Modifier */
.c-local-menu__list__item--typeB { … } /* Modifier */
.c-local-menu__list__item--category { … } /* Modifier */
.c-local-menu__list__item.is-active { … } /* State */
<nav class="c-local-menu">
<h2 class="c-local-menu__title">メニュー見出し(Basis)</h2>
<ul class="c-local-menu__list">
<li class="c-local-menu__list__item local-menu__list__item--typeA is-active"><a href="#">メニューA</a></li>
<li class="c-local-menu__list__item local-menu__list__item--typeB"><a href="#">メニューB</a></li>
</ul>
</nav>
<nav class="c-local-menu c-local-menu--category">
<h2 class="c-local-menu__title">メニュー見出し(Modifier)</h2>
<ul class="c-local-menu__list c-local-menu__list--category">
<li class="c-local-menu__list__item local-menu__list__item--typeA local-menu__list__item--category is-active"><a href="#">メニューA</a></li>
<li class="c-local-menu__list__item local-menu__list__item--typeB local-menu__list__item--category"><a href="#">メニューB</a></li>
</ul>
</nav>
現在地表示部分が SMACSS の状態を示すマルチクラス記法になったので、クラス名の重複分がなくなり、ちょっとだけスッキリしました。こうなると、Modifier も全部マルチクラスにしてはどうだろうか、と思い始めてきます。
識別子の付いていないクラス名は全て Modifier
ここからが独自記法のルールになります。
FLOCSS ではクラスに識別子を付けて管理しているため、逆を返せば、識別子が付いていないクラスはすべて Modifier というルールにしてしまうことで、マルチクラス記法に統一することができます。
.c-local-menu { … } /* Block */
.c-local-menu.category { … } /* Modifier */
.c-local-menu__title { … } /* Element */
.c-local-menu__list { … } /* Element */
.c-local-menu__list.category { … } /* Modifier */
.c-local-menu__list__item { … } /* Element */
.c-local-menu__list__item.typeA { … } /* Modifier */
.c-local-menu__list__item.typeB { … } /* Modifier */
.c-local-menu__list__item.category { … } /* Modifier */
.c-local-menu__list__item.is-active { … } /* State */
<nav class="c-local-menu">
<h2 class="c-local-menu__title">メニュー見出し(Basis)</h2>
<ul class="c-local-menu__list">
<li class="c-local-menu__list__item typeA is-active"><a href="#">メニューA</a></li>
<li class="c-local-menu__list__item typeB"><a href="#">メニューB</a></li>
</ul>
</nav>
<nav class="c-local-menu category">
<h2 class="c-local-menu__title">メニュー見出し(Modifier)</h2>
<ul class="c-local-menu__list category">
<li class="c-local-menu__list__item typeA category is-active"><a href="#">メニューA</a></li>
<li class="c-local-menu__list__item typeB category"><a href="#">メニューB</a></li>
</ul>
</nav>
このルールには以下のようなメリットがあります。
- 記述が HTML では
class="c-label-menu category"
、CSS では.c-label-menu.category
とほぼ同じ構成で記述できるため、両方のコードから関連性を参照しやすい - 人間がクラス定義を読んでも文脈として意図を汲み取りやすい
- 重複記述が減り、コード量も減少する
- CSS 上では Modifier は必ずマルチクラスで Block や Element とセットで記述するため、別のコンポーネントとクラス名の衝突が発生せず、意図しない上書きが発生しない
BEM 記法の圧縮
個人的な好き嫌いかもしれませんが、やはり区切り文字が記号2文字は冗長に感じます。
また、FLOCSS の識別子記法でハイフン記号を使っているため、Block に相当するクラスには視認性の点からもあまり記号を使いたくありません。
Element の文字区切りは1文字として、単語の区切りは Lower Camel Case にすることでクラス名の表記に調子がついて視覚的にだいぶ読みやすくなります。
.c-localMenu { … } /* Block */
.c-localMenu.category { … } /* Modifier */
.c-localMenu_title { … } /* Element */
.c-localMenu_list { … } /* Element */
.c-localMenu_list.category { … } /* Modifier */
.c-localMenu_list_item { … } /* Element */
.c-localMenu_list_item.typeA { … } /* Modifier */
.c-localMenu_list_item.typeB { … } /* Modifier */
.c-localMenu_list_item.category { … } /* Modifier */
.c-localMenu_list_item.is-active { … } /* State */
<nav class="c-localMenu">
<h2 class="c-localMenu_title">メニュー見出し(Basis)</h2>
<ul class="c-localMenu_list">
<li class="c-localMenu_list_item typeA is-active"><a href="#">メニューA</a></li>
<li class="c-localMenu_list_item typeB"><a href="#">メニューB</a></li>
</ul>
</nav>
<nav class="c-localMenu category">
<h2 class="c-localMenu_title">メニュー見出し(Modifier)</h2>
<ul class="c-localMenu_list category">
<li class="c-localMenu_list_item typeA category is-active"><a href="#">メニューA</a></li>
<li class="c-localMenu_list_item typeB category"><a href="#">メニューB</a></li>
</ul>
</nav>
子孫セレクタを使用して Modifier の記述を Block 要素のみに集約
BEM 記法では子孫セレクタを使わずにフラットに書いていくため、Modifier による派生スタイルを Element に適用していくのに新たなクラス名を付ける必要がありましたが、構成要素が変わらないのであれば、Modifier は Block にのみ適用した方が HTML 側のクラス定義をさらにシンプルにできます。
.c-localMenu { … } /* Block */
.c-localMenu_title { … } /* Element */
.c-localMenu_list { … } /* Element */
.c-localMenu_list_item { … } /* Element */
.c-localMenu_list_item.typeA { … } /* Modifier */
.c-localMenu_list_item.typeB { … } /* Modifier */
.c-localMenu_list_item.is-active { … } /* State */
.c-localMenu.category { … } /* Modifier */
.c-localMenu.category .c-localMenu_list { … } /* Modifier - Element */
.c-localMenu.category .c-localMenu_list_item { … } /* Modifier - Element */
<nav class="c-localMenu">
<h2 class="c-localMenu_title">メニュー見出し(Basis)</h2>
<ul class="c-localMenu_list">
<li class="c-localMenu_list_item is-active"><a href="#">メニューA</a></li>
<li class="c-localMenu_list_item"><a href="#">メニューB</a></li>
</ul>
</nav>
<nav class="c-localMenu category">
<h2 class="c-localMenu_title">メニュー見出し(Modifier)</h2>
<ul class="c-localMenu_list">
<li class="c-localMenu_list_item is-active"><a href="#">メニューA</a></li>
<li class="c-localMenu_list_item"><a href="#">メニューB</a></li>
</ul>
</nav>
HTML 上で Element に定義していた Modifier を Block に一本化したため、category の有無でこのコンポーネントの派生を切り替えることができるようになりました。
これは JavaScript やプログラムから条件によって Modifier を付与する必要がある場合、複数の要素に対してクラスを適用しなくて済むというメリットがあります。
Sass で記述を圧縮
HTML が簡潔になった代わりに CSS の方が子孫セレクタの記述をした分、ずいぶんと冗長な個所が増えてしまいました。
ここで Sass を使って冗長になった記述を圧縮します。
.c-localMenu {
$c: &;
@at-root &_title { … }
@at-root &_list {
…
@at-root &_item {
…
&.typeA { … }
&.typeB { … }
&.is-active { … }
}
}
&.category {
…
#{$c}_list {
…
@at-root &_item { … }
}
}
}
Block 名を最初の記述のみとし、後続の記述はすべて「&」を使用して参照することで Block 名が何度も登場することを避けています。Block のクラス名を変更すれば自動で関連する Element にすべて反映されるようになります。
また、直下の $c: &;
は Block のクラス名を格納しています。なぜこのような形を取るかというと、Block の Modifier 以下、子孫セレクタを展開する際に、最初の Block と同じように参照すると、以下の例のようにうまく展開されないためです。
.c-localMenu {
@at-root &_list {
@at-root &_item {
}
}
&.category {
@at-root &_list {
@at-root &_item {
}
}
}
}
/*
.c-localMenu { … }
.c-localMenu_list { … }
.c-localMenu.category { … }
.c-localMenu.category_list { … }
.c-localMenu.category_list_item { … }
と出力され、想定した子孫セレクタが得られない。
*/
「&」は直近の親のクラス名を継承するので、Block 名定義直後に変数にクラス名を保存して、Modifier 内の定義で変数を再利用することで、この問題を回避しています。
Element 定義の重複記述を回避
前項の Scss で、ある程度の目的は達成できたものの、Modifier の記述時に標準定義の Element が再登場するため、結果的に表記が重複してしまいます。また、Modifier 直下は意図した展開がされるよう、@at-root
を使わない、といった配慮が必要になります。
Block の規模が小さければ大したことがないのですが、派生の多かったりコード行数の多い Block だと、標準定義と Modifier 定義の間を行ったり来たりする手間もかかります。
これらを解決するため、Modifier 定義を以下の形に変更します。
.c-localMenu {
$c: &;
&.category {
…
}
@at-root &_title { … }
@at-root &_list {
…
#{$c}.category & {
…
}
@at-root &_item {
…
#{$c}.category & {
…
}
&.typeA { … }
&.typeB { … }
&.is-active { … }
}
}
}
前述の定義の仕方と比べて「category」の Modifier 定義が各項目に分散してしまいますが、Modifier 内に 標準定義の Element を再度記述する必要がないため、重複表記を省略することができます。
また、Modifier の記述は基本的には標準定義をベースに上書きしていくため、前述の記述よりも上記の方が上書き対象の定義同士の位置が近く、最終的な内容の見通しが立ちやすいため、この形になりました。
Sass 記法再考(2016.02.14 追記)
トップレベルのModifierを記述する分には問題ないのですが、子階層の Element の Modifier からの Element を定義すると Sass 記法がどうにも破綻してしまいました…
例えば、以下のようなHTMLがあり、
<nav class="c-localMenu category">
<h2 class="c-localMenu_title">メニュー見出し(Modifier)</h2>
<ul class="c-localMenu_list">
<li class="c-localMenu_list_item is-active"><a href="#" class="c-localMenu_list_item_link">メニューA</a></li>
<li class="c-localMenu_list_item"><a href="#" class="c-localMenu_list_item_link">メニューB</a></li>
</ul>
</nav>
マルチクラスで Modifier -> Element -> Modifier -> Element を参照しようとすると、
.c-localMenu.category .c-localMenu_list_item.typeA .c-localMenu_list_item_link
のような記述になります。ちょっとこれはグッタリ来ますね…
Sassで書き直すと以下のような形。
.c-localMenu {
$c: &;
&.category {
…
}
@at-root &_title { … }
@at-root &_list {
…
#{$c}.category & {
…
}
@at-root &_item {
…
#{$c}.category & {
…
}
@at-root &_link {
…
}
&.typeA {
…
#{$c}.category & {
…
#{$c}_list_item_link {
…
}
}
}
&.typeB { … }
&.is-active { … }
}
}
}
トップレベルと中間レベルの Modifier 記述が混在して見通しが悪くなりました… これならば、初回の Sass 記法で素直に書いたほうが良さそうです。
記述を再整理してみた結果が以下。同じ出力になる @at-root
は省略します。
.c-localMenu {
$c: &;
&_title {
…
}
&_list {
…
&_item {
…
&.typeA { … }
&.typeB { … }
&.is-active { … }
&_link {
…
}
}
}
&.category {
…
#{$c}_list {
…
&_item {
…
&.typeA {
…
#{$c}_list_item_link {
…
}
}
&_link {
…
}
}
}
}
}
#{$c}_list_item_link
あたりの表記は Element の二重記述になるので、何とか書かなくてもよい方法を模索しましたが、中間変数や配列を用意しても、結局呼び出し定義が煩雑になるので、あまり良い手立てはありませんでした。
おわり
以上、BEM から始めた記法を無駄のない記法にカスタマイズした結果を晒してみました。
従来の記法に比べて「コード量が少ない」「クラス定義が文脈として読み取れる」「定義が衝突しにくい」が最大のメリットです。