CSS設計の影響範囲は?
CSS設計はWEBアプリケーションの品質維持に大きく関わり、長期に渡ってチームメンバーに影響がある話です。それは、viewを組み立てる際のDOM構造設計そのもので、様々なコードからDOMが参照されます。ご存知の通り、グローバル汚染と脆い性質上、命名規則や規約・概念をもって乗り切るしかないのが現状です。
CSS Modules がこの問題を解決してくれる日も遠くない様に思いますが、しばらくの間はこの問題と向き合わなければいけません。本稿は新しいCSSプリプロセッサーの話も無ければ、CSS in JS の話もありません。「こんなCSS設計にするにはどうすれば良い?」と考え書き起こしたガイドラインの話です。
破綻しない。無駄が無い。運用し易い。拡張し易い。同じ記述が少ない。
同じバグを踏まない。多デバイスで共有出来る。リニューアルにも堪えられる。
有名なCSS設計から、良いとこどりをしているだけですが「実際にsassでどう書くか?」という細かいところまで言及しています。以下のプラクティスに興味のある方には有意義な内容になるかもしれません。
- BEMによるコンポーネントスコープ
- MCSSによるレイヤー責務
- ITCSSによる詳細度の解決
- AtomicDesignによるコンポーネント粒度明示
- OOCSSによる多態性
本稿で紹介しているセレクタはBEMの命名規則を継承しています。キャメルケースや、チェイン、アンスコ2つなどは「生理的に無理」と感じるかもしれません。これは私が初めてBEMを見た時にそうだった様に「何故こんな長ったらしい命名規則をつけるのか?」という気持ちがよく分かります。BEMは命名規則以前に、CSS Modules が提供してくれるコンポーネントスコープをCSSだけで体現化している点に最も意義があります。
命名規則は機能の区別が出来て統一されていれば、ハイフンもアンスコもいらないし、重要なのは一つのセレクタの中にこれだけ機能が詰まっている ことがお伝え出来ればと思っています。以下の図は、このガイドラインで現れるセレクターの一例です。
パスカルでもチェインでもスネークでも、各色ご自由に脳内変換して頂けると幸いです。
私自身も利用しているスタイルガイドラインツールの制限のため、このような命名規則を選びました。
設計を考えながら目指していたもの
【変化に強い構造は?】
DOM構造や装飾に依存したセレクタにしてしまうと、リファクタリングの際に、html・cssだけでなく、javascriptやテストコードも修正しなければいけません。抽象度の低いセレクタ名は、見栄えの変更をCSSの修正だけで済ますことができます。以下は、装飾による抽象度の高いDOMは変化に弱いということを示す例です。
.e-btnRoundRed // 抽象度の高い命名
.c-articlesListItem__btnLearnMore // 抽象度の低い命名
例えば、上記の様な指定で、全く同じ見栄えのボタンがあるとします。(後者のセレクタは前者を@extend
している) もし、前者のクラス名に依存したテストコードがあった場合、ボタンの装飾が変わったタイミングでテストコードも修正しなければいけません。
【メンテナンスし易い状態】
cssはセレクタさえ一致すれば、どこからでも指定を受けてしまい、詳細度がさらに問題を複雑にします。コンポーネントが持つ指定を一元管理し、影響を与える範囲を限定します。そうすることで、新しい機能の実装に集中することができ、バグの早期発見に繋がります。詳細度の衝突による無駄なoverrideも極力抑えることが出来ます。
・ 異なるコンポーネントから指定を受けないこと
・ 異なるコンポーネントの指定をしないこと
上記のルールに則ることで、疎結合で再利用可能な状態を保つことができます。
【DRYの原則に則る】
本稿ガイドラインではmixinやplaceholderだけでなく、既存のコンポーネントを@extend
することを推奨する箇所があります。特定のブラウザバグを一箇所で修正することができ、含有要素を拡張させたい場合も、一元修正で済むためです。
【スタイルガイドの導入を容易に】
スタイルガイドを導入することで、チーム間で定義済みのコンポーネントを共有することが出来ます。本稿で紹介しているガイドラインでは、コンポーネントを一元管理する特徴から、ガイドライン化が容易になります。コンポーネント再利用の際に、必要なコンポーネントを探す手間を省くことができます。スタイルガイドツールにはSC5 Style Guide Generatorを利用します。
【DOM構造とCSS定義ファイルの延命】
パラダイムシフトから一歩引いた枯れたテクニックを集約しているため、フロントエンドの潮流に左右されにくいです。従来のMVCフレームワークやSPAでも共有できる、疎結合なCSSコンポーネントを目指します。
設計で最も重要だと考えた、5つの概念
- コンポーネント粒度レイヤー
- コンポーネントの一元管理
- レイアウトの分離
- 識別子の定義
- 詳細度の管理
冒頭でも挙げたとおり、倣っているCSS設計は以下の5つです。
BEM / MCSS / ITCSS / AtomicDesign / OOCSS
コンポーネントをどの粒度で線引きするのか? という点は、様々なCSS設計で提唱されています。本稿ガイドラインでは、AtomicDesignからコンポーネント粒度の概念を引用していますが、呼称が異なり、他のCSS設計に近しいものになります。さっそくコンポーネント粒度について説明していきます。
【概念1】コンポーネント粒度レイヤー
「コンポーネント粒度レイヤー」とは、UIコンポーネントの密度・用途・ルールによってレベル分けをしたものです。本稿ガイドラインでは7つに分類しています。
・ LV0.基礎【Foundations】
・ LV1.独立要素【Elements】【.e-】プレフィクスを付与
・ LV2.複合要素【Modules】【.m-】プレフィクスを付与
・ LV3.統合要素【Components】【.c-】プレフィクスを付与
・ LV4.レイアウト【Layouts】【.l-】プレフィクスを付与
・ LV5.テンプレート【Templates】【.t-】プレフィクスを付与
・ LV6.アプリケーション【Application】
LV0.基礎【Foundations】
AtomicDesignの「原子」の定義には、タイポグラフィや色の情報も含まれています。この粒度はLV0.基礎【Foundation】となります。多くのCSS設計でも、この粒度レイヤーは統合し、Foundation、settings、toolsなどの名称で粒度レイヤーを統合しているケースがほとんどです。コンポーネント作成における土台となる当該レイヤーは、2種類に分類されます。
【vars】 色変数 / レイアウト変数
【mixins】 ミックスイン
上記は展開先で初めてCSSとして出力されるものです。settings、toolsにあたります。
【resets】 ブラウザ初期値リセット
【bases】 タグ固有の指定
【decorators】 %による1つか2つの装飾に関するプレースホルダ
こちらは@importすると、そこで出力が発生するグループです(プレースホルダは利用時)。Foundationと呼ぶに相応しい、設計の土台となります。
LV1.独立要素【Elements】【.e-】プレフィクスを付与
独立した最小単位のコンポーネントが帰属する粒度レイヤーです。命名規則の特徴としては、具体的な見栄えを表現する抽象度の高いものです。マークアップ時にこのセレクタはなるべく使用せず、上位粒度レイヤーにどの様な見栄えの部品を取り込んでいるのか解りやすい名称にします。
ボタン / アイコン / 見出し / 装飾された矩形 など
LV2.複合要素【Modules】【.m-】プレフィクスを付与
プロダクトにおいて汎用性の高いコンポーネントが帰属する粒度レイヤーです。LV1を@extend
で取り込み、BEM-Elementの指定で隠蔽しながら構成します。取り込んだLV1の子要素をmodifyする時のみタグ名の指定をすることが可能です。リストやボタングループ、1つの機能を提供するUIコンポーネントです。
ページネーション / タブ / モーダル / リスト / 注釈 など
LV3.統合要素【Components】【.c-】プレフィクスを付与
プロダクトにおいて一意性の高いコンポーネントが帰属する粒度レイヤーです。エンドポイントやSPAコンポーネントClass名に由来する、冗長な名前空間で指定します。LV1をBEM-Element化したり、レイアウトコンテナにあたるBEM-Elementを用意しLV2を配置します。一意性の高い要素は必ずBEM-Elementとして定義します。
ヘッダー / フッター / 見出しを含む一覧 / 固定ページのセクション など
LV4.レイアウト【Layouts】【.l-】プレフィクスを付与
デバイス特有のレイアウトを定義する、LV3をレイアウトするための粒度レイヤーです。デバイス毎に、特徴的なレイアウト指定をするコンテナを指定します。DOMコンポーネントをマークアップする際は、デバイス間での再利用を維持するためにLV3とこの粒度レイヤーが混在しない様に注意します。また内包するコンポーネントをmodifyする指定はなるべく避けます。このレイヤーの存在により、デバイス毎にDOMが異なるpartialファイルを用意しなくてもよくなる可能性が高まります。
LV5.テンプレート【Templates】【.t-】プレフィクスを付与
ページ全体のラッパーコンポーネントです。ページ全体に掛かる装飾を施したり、状態に応じたmodifyを与えます。Elementは持たない薄いレイヤーです。
LV6.アプリケーション【Application】
詳細度100以上の指定をする必要がある場合に利用するレイヤーです。!important による指定を避けるために利用しますが、利用されない状態が望ましいです。
【概念2】コンポーネントの一元管理
大元になるscssファイルの内容は以下の様になります。ファイルの並び順や詳細度管理については、ITCSSを倣います。ただし、ファイルツリーはITCSSのそれと異なり、深いディレクトリのネストを用います。エントリーポイントとなるファイル名は _.scss
で統一します。
// 出力されない指定
@import "./foundations/vars/_";
@import "./foundations/mixins/_";
// 出力される指定
@import "./foundations/reset/_";
@import "./foundations/base/_";
@import "./foundations/placeholders/_";
// 名前空間を持ったコンポーネントCSS定義
@import "./elements/_";
@import "./modules/_";
@import "./components/_";
@import "./layouts/_";
@import "./templates/_";
sassの親参照セレクタ「&」はファイル間をまたぐ事が出来ます。これを利用すれば、ディレクトリツリーと同名の名前空間(コンポーネント/プレースホルダ名)を確保する事が出来、管理が容易になります。この中でBEMの記法にのっとったセレクタを定義します。
.c {
// .c 詳細度 10
@import "articles/_";
}
&-articles {
// .c-articles 詳細度 10
@import "list/_";
}
&List {
// .c-articlesList 詳細度 10
&__thumbnail {} // .c-articlesList__thumbnail 詳細度 10
}
この様にディレクトリツリーとコンポーネント名称が一致するため、修正など発生した場合に該当ファイルの在りかが一目瞭然で、定義が重複するリスクを避けることができます。
占有した名前空間以外から、そのコンポーネントが受ける指定を分散させると管理が難しくなります。名前空間の中で全ての状態を管理出来ていることが望ましいです。この時、自身が子孫セレクタにあたる指定を含めてはいけません。将来どのコンポーネントに内包されるか予測が出来ず、思わぬ崩れを起こす危険性があるからです。(ただしLV5,V6による指定は除く)
CS5-StyleGuide
/*
Markup:
<div class="c-articlesList">
<ul class="c-articlesList__items">
<li class="c-articlesList__item">c-articlesList__item</li>
<li class="c-articlesList__item">c-articlesList__item</li>
</ul>
</div>
Styleguide 3.c-articlesList
*/
&List {
&__items {} // 詳細度10
&__item {} // 詳細度10
&._show_ {} // 詳細度20
}
コンポーネントのファイルを細かく分割することで、スタイルガイドツールが導入し易い状態になります。スタイルガイドツールに必要なコメントルールセットを上部に挿入します。
ツールの導入方法については割愛します。下記記事を参考にさせていただきました。
sc5-styleguideを使用したのでまとめてみました
【概念3】レイアウトの分離
**コンポーネントは「mergin, top, bottom, left, right, float」など、自身のレイアウトに関する指定をしてはいけません。**指定をしてしまうと、想定外の親コンポーネントで再利用しようとした時、そのプロパティを都度上書きする必要が出てきます。このルールに則ることで、汎用性の高いコンポーネントを定義出来ます。
レイアウトは親の粒度レイヤーで確保されるレイアウトコンテナに委ね、そこにコンポーネントを格納します。レイアウトコンテナはネストが深くなりますが、想定外のコンポーネントが追加されても修正に必要なコストを最小限に抑えることが出来ます。
以下例の様に、内包する別コンポーネントの存在に依る指定をしてしまうと、メンテナンス性が低下することが分かるかと思います。これらのことから、名前空間の異なるコンポーネントの指定を含んではいけません。
<div class="c-articles">
<h2 class="c-articles__title"></h2>
<div class="c-articles__list">
<ul class="m-list">
<li class="m-list__item"></li>
<li class="m-list__item"></li>
</ul>
</div>
</div>
&-articles {
&__title {
@extend .e-titlePrimary;
margin-bottom: 10px;
float: left;
}
// x 良くない指定
.m-list { // 詳細度20
// 子孫セレクタによる広範囲の指定となってしまう点が問題
// 異なるのコンポーネントに同じレイアウトを適用したい場合、汎用性がない点が問題
// このコンポーネントが存在していることに依存している点が問題
// 詳細度を高くしてしまう点が問題
margin-bottom: 10px;
float: right;
}
// o 良い指定
&__list { // 詳細度10
// レイアウトコンテナの指定。ネストが一つ増えてしまうが拡張性が高い
margin-bottom: 10px;
float: right;
}
}
【概念4】識別子の定義
上記のセレクタ解説図右側に「識別子」というものが示されています。マルチクラスや子要素を定義する際に使用する命名規則です。以下の利点が得られます。
Modifier識別子
参考設計:【OOCSS】
BEMに見られる冗長なModifier「--」は利用しません。その代わりコンテキストに閉じた 「Modifier識別子」 で異なる状態を派生させます。本設計でいうModifier識別子とは、抽象度の高い名称をアンダースコアで囲んだものです。BEM-Modifierは冗長でhtmlの可読性が悪くなることから、Modifierについてはマルチクラスとしています。この識別子をもつModifierはGLOBALに定義していけません。
.c { // .c 詳細度10
&-articles { // .c-articles 詳細度10
&List { // .c-articlesList 詳細度10
&._wide_ {} // .c-articlesList._wide_ 詳細度20
&._min_ {} // .c-articlesList._min_ 詳細度20
&._1_ {} // 数字だけの命名も出来るので、プリプロセッサ等で活用しやすい
&._2_ {}
}
}
}
Modifier識別子を利用することで、出力されるcssの記述量が大幅に減ります。そして抽象度の高い安易なグローバル指定がプラグインなどの外部要因で定義されてしまっても、この識別子が侵犯を防いでくれ、安心して抽象度の高い命名をすることが出来ます。CSSプリプロセッサやjavascriptからの操作もシンプルになります。
Element識別子
コンポーネントの末端要素に使用することが出来る、アンダースコア2つの接頭辞を持つセレクタ名です。抽象度が高いため、Element識別子が指定されたタグは内部構造を変更してはいけません。DOM構造が複雑でありながらも、将来的に変化しないコンポーネントに限り使用することが出来ます。上手く活用すれば、CSSファイルの肥大化を防ぐことができます。
.c {
&-articles {
&List {
&__item {
.__icon {}
.__tag {}
}
}
}
}
【概念5】詳細度の管理
参考設計:【MCSS / ITCSS】
ここまでの規約に則った指定が出来ていれば、粒度レイヤーLV5までの指定は全て、詳細度100以下収まるはずです。LV6.アプリケーション【Application】レイヤーでは、唯一IDを用いた指定が出来ます。状態が複雑に絡み、詳細度の競合から打ち消しが効かない指定を、!importantを利用せずに解決することが出来ます。ただし、一元管理されたセレクタ同士が詳細度で競合するというケースはほとんど考えられません。ABテストなどで利用するケースがほとんどでしょう。
#app { // #app 詳細度100
.c { // #app .c 詳細度110
&-articles { // #app .c-articles 詳細度110
&List { // #app .c-articlesList 詳細度110
&._wide_ {} // #app .c-articlesList._wide_ 詳細度120
}
}
}
}
この粒度レイヤーは、他の粒度レイヤーとは異なる指定方法をとります。一元管理の原則が遵守できる様に、名前空間に閉じたファイル内で以下の様に定義します。
&Grid { // .c-articlesListGrid
&__thumbnail {} // .c-articlesListGrid__thumbnail 詳細度10
}
#app._state_ &__thumbnail {} // #app._state_ .c-articlesListGrid__thumbnail 詳細度120
appendix : 禁止事項とBEMの基礎知識
タグ名を含む指定はDOMのリファクタに弱く、また詳細度を上げるため、利用しない様にします。この指定を打ち消すために、以降はより詳細度を高くしたセレクタを使用しなければいけなくなります。
h2.title {} // 詳細度 11
.title {} // 詳細度 10
直下セレクタはコンポーネントのコンテキストに閉じるための良いアイデアですが、タグ名を含む指定と同様、DOMに依存した指定になるためなるべく使用しない様にします。コンテキストに閉じることが出来、DOM構造の可変に強く、詳細度を上げることのないBEMは、CSSコンポーネント設計には必須といえます。
【DOM変化による直下セレクタの破綻】
.c {
> ul > li {} // 詳細度12
&List__item {} // 詳細度10
}
<ul class="c-list">
<li class="c-list__item"></li>
<li class="c-list__item"></li>
</ul>
<div class="c-list"> <!-- divに変更になった -->
<ul class="c-list__itemGroup"> <!-- wrapperが追加された -->
<li class="c-list__item"></li> <!-- > ul > li の指定が無効になる -->
<li class="c-list__item"></li> <!-- > ul > li の指定が無効になる -->
</ul>
</div>