CSSのコーディングにおいて、コーディングガイドラインが決められていなかったり、プロジェクト内のチームメンバー同士でコーディングスタイルを統一できていなかったりすると、それぞれが独自スタイルでコーディングし始めるので非常に混乱することになります。
さらにレイアウト部分と視覚的なデザインを混ぜてしまうと一貫性がなく複雑なコードになってしまいます。
これらを解消するための指針として、Drupal CSS コーディング スタンダードで規定されたガイドラインがあります。
#CSSコーディングの課題
これらの課題を解消するための設計手法を紹介していきます。
- 複数メンバーでCSSコーディングしたら似たようなコードが散乱していた
- 適当にコードを書いていたら設計が複雑になりファイルが膨れあがった
- 1箇所修正しただけなのに他のページが表示崩れした
- ページ数が多い分、作業時間も多くかかった
- そもそもコーディングルールが分からない
#CSS設計にSMACSSの考え方を導入する
Drupal CSS コーディング スタンダードでは、Drupal8からSMACSSが推奨されています。
CSS file organization (for Drupal 8)
CSS architecture (for Drupal 8)
SMACSSとは、Scalable and Modular Architecture for CSSの略語で「スマックス」と読みます。
SMACSSはCSSの設計手法のひとつで、CSSのルールを概念的に5つにカテゴライズした上で、それぞれの考え方や記述ルールが取り決められているのが特徴的です。
##5つのカテゴリ
ベース
HTML要素(以下、要素)そのもののデフォルトスタイル
レイアウト
ページの骨組み部分。headerやfooterなどのエリアごとに定義する
モジュール
再利用可能なパーツ
※Drupal8のCSS コーディングスタンダードでは”コンポーネント”と呼んでいる
ステート(状態)
レイアウトやモジュールの特定の状態を示す
テーマ
サイトの視覚的な部分
##SMACSSを導入するメリット
- どのプロジェクトでも一貫した設計になる
- 繰り返し項目のパターン化によってコード量が少なくなる
- メンテナンス性が向上する
まとめると、SMACSSの導入メリットは、小規模から大規模プロジェクトに対応できるのと、レイアウトとモジュールを独立させることで、どのプロジェクトでも一貫した設計を行い、コードの再利用性もアップします。
#ベース
ベースルールは、要素セレクタ、子孫セレクタ、または子セレクタを擬似クラスと共に要素にスタイル適用します。
クラスセレクタやIDセレクタは使いません。ページ上のすべての要素をどう見せたいかについて、デフォルトのスタイルを定義します。
例)ベースのルールセット
body, form {
margin: 0;
padding: 0;
}
a {
color: #039;
}
a:hover {
color: #03F;
}
##ベースの役割
ベーススタイルには、見出しサイズ、テキストリンクの見た目、及びフォントスタイル、および本文背景の設定も含みます。!importantは使用することはありません。
ベースでやることは、あくまでmarginやpaddingを0をセットしたり、borderのスタイルを削除したりなどのリセットのみに留めておくと設計しやすくなります。
####注意点
要素セレクタに対して具体的なスタイルを指定をしてしまうと、後々レイアウトやテーマの中でベースのスタイルを打ち消す作業が発生してしまいます。
##ノーマライズCSS/リセットCSSはベースで定義する
###リセットCSS(reset.css)
リセットCSS(reset.css)と呼ばれる基本スタイルセットがあり、デフォルトのマージン、パディング、および各種プロパティを削除してくれるスタイルセットです。
その目的はサイトを構築するためにブラウザ間の違いを無くしてイチから基盤を再定義することです。
###ノーマライズCSS
ノーマライズCSSは、ブラウザのスタイルをすべて削除するのではなく有用なデフォルトスタイルは残した上で、ブラウザ間の一貫性のなさを修正するためのスタイルセットです。
例えば、Compassを使っている場合はこのようなコードを書いて定義します。
例)SCSS+Compass環境での リセットCSS 定義方法
@include 'compass/reset';
例)SCSS+Compass環境での ノーマライズCSS 定義方法
@import "normalize";
Compassが提供するスタイルセット以外にも、GitHub上には有用な ノーマライズCSS/リセットCSS が公開されているようなので、それらを利用しても構いません。
※2017年6月時点でCompassの開発は停止しており、今後のサポートは得られない状況なのでGitHub上の最新スタイルセットを利用した方が最新ブラウザ状況に対応できるはずです。
###リセットCSSの問題点
多くのリセットCSSは過剰にスタイルを削除するものであり、マージンとパディングを要素から取り除く為だけの目的で導入するとしたら問題を引き起こす可能性があります。もし導入すれば新たな作業が発生して、CSSコード量が増えてしまいます。
そのため基本的にリセットCSSは使わずに、ノーマライズCSSを利用しましょう。
ノーマライズCSSは、ベースの先頭で定義するようにしてください。
#レイアウト
その名の通り、要素を使ってページ上でレイアウトを設計することです。
レイアウトのスタイルにはメジャーコンポーネントとマイナーコンポーネントという2つの概念があります。
- メジャーコンポーネント ・・・ ヘッダーやフッターなどのサイト共通のもの
- マイナーコンポーネント ・・・ 検索フォームや繰り返し項目などのことで、ページ上で使い回せるもの
基本的にメジャーコンポーネントの配下にマイナーコンポーネントが存在することになります。
しかし再利用性を考えてメジャーコンポーネントとマイナーコンポーネントは分離して定義しておきましょう。
##レイアウトはidとクラスのどちらを使えばいい?
ヘッダーやフッターなどの主要なレイアウトスタイルは、ページ上で複数回利用するものではないので、従来通りの考え方だとIDセレクタを使用してスタイルが設定されていました。
例)従来のidセレクタを使ったレイアウト
#header, #article, #footer {
width: 960px;
margin: auto;
}
#article {
border: solid #CCC;
border-width: 1px 0 0;
}
SMACSSでは、レイアウトのセレクタにid,クラスのどちらを使うのかを厳密に定められておらず、どちらを使っても構いません。
##レイアウトのセレクタにクラスを使う
クラスセレクタを使うときは、レイアウトのためのクラスだと分かるようにプレフィックスを付けます。SMACSSで推奨しているのは layout- または l- です。 コンテンツの繰り返し項目は grid- でも良いでしょう。
.l-header {
width: 100%;
margin-bottom: 20px;
}
.l-footer {
width: 100%;
border-top: 1px solid #000;
}
.l-main {
float: left;
width: 80%;
}
.l-sidebar {
float: right;
width: 20%;
}
#モジュール
モジュールは、ページを構成する個々のコンポーネント(部品)のことです。そして、特定のページやレイアウトに依存しないようにスタンドアロンコンポーネントとして設計する必要があります。そうすることで、ページはより柔軟に設計できるようになります。
モジュールのルールセットを定義するときは、セレクタにはクラス名のみを使用してIDと要素名は使用しません。
モジュールはさまざまな要素で利用される可能性があるので特定の要素に依存しないようにしましょう。
特定の要素のためにモジュールを作りたければ、子セレクタや子孫セレクタでの対応になります。
##再利用性を高める
基本的には単一クラスのセレクタ、または子セレクタまでに限定しましょう。子孫セレクタを使ってしまうと再利用性が低下してしまいます。
例)モジュールを特定の要素に対応する
/** 単一クラスのセレクタ ・・・ OK **/
.module01 {
padding: 5px;
}
/** 子セレクタ ・・・ OK **/
.module02 > h2 {
padding: 5px;
}
/** 子孫セレクタ ・・・ NG **/
.module02 .title {
padding: 5px;
}
※子孫セレクタを禁止するわけではありません。状況に合わせて判断してください。
モジュールの命名規則はとくに決められていません。ぱっと見て用途が分かるような名前を付けてあげましょう。
##要素セレクタを使うときの注意点
サイト全体の設計を見渡した上で、要素セレクタを使ったとしてもコードが複雑にならないと判断した場合、要素セレクタとともに子または子孫セレクタを使用します。
例)要素セレクタの使用
/** HTMLコード **/
<div class="fld">
<span>Folder Name</span>
</div>
/** 子セレクタ **/
.fld > span {
padding-left: 20px;
background: url(icon.png);
}
プロジェクトが複雑になるにつれて、コンポーネントの機能を拡張する必要性が高くなります。
再利用可能なコンポーネントに要素セレクタを使用していると、後々デザインのバリエーションを増やすときなどに拡張性が制限される可能性が高くなります。
とくにspanやdivなどの一般的なHTML要素をセレクタとして使ってしまうとほかのモジュールやレイアウトと競合しがちです。
そのため、汎用的に使いたいコンポーネントはなるべくクラスを付けるようにしておき、ルールセットを作るときの曖昧さを取り除いておきます。
##セレクタにおけるid、クラス、要素名の使い分け
###idとクラスの違い
- idセレクタはHTML5の仕様上、同じページ内で複数利用できない。HTMLバリデーターでエラーになる。
- 基本的にidは基本的にjsがハンドリングするためのもの。
- javascriptはページ上に1つしか存在しない前提で動作する。
例えば、getElementById() は始めにヒットしたidのみを対象としている。 - jQueryでセレクタを指定するとき、クラスよりidの方が高速に動作すると言われている。
- cssにおいてはidとクラスの挙動に違いはないが、idセレクタはCSSの詳細度が高いのでスタイルの上書きが難しくなる。(※)
※子孫セレクタを5個も6個も繋げた場合はクラスでも要素名でもスタイルの上書きは難しくなります。
###セレクタの詳細度
CSSには詳細度という概念があります。
スタイルが重複したときに、ブラウザがそのスタイルを採用する優先度です。
詳細度を低い順に並べると、全称セレクタ(*) → 要素名 → クラス → id です。
idセレクタは詳細度が高くなるので使うのを避けたいですが、ページ上で1度しか使わないことを明確にするためにあえて使うという考え方はアリです。
###不要な要素名は付けない
パフォーマンス面では、階層が深くなるほどレンダリングが遅くなると言われていますが、クラスとidの先頭に要素を付けても遅くなると言われているので付けないようにしましょう。
例)不要な要素名が付いている
/* OK */
.header {...}
/* NG */
div.header {...}
###セレクタの深度は浅くする
セレクタの深度は浅いほどメンテナンス性とパフォーマンスを向上します。
例)子孫セレクタよりも子セレクタの方が深度が浅い
/* OK (子セレクタは深度が浅い) */
.branding > h1 {...}
/* NG (子孫セレクタは深度が深い) */
.branding h1 {...}
例)セレクタ階層は深すぎるのはNG
/* OK (セレクタ階層が浅い) */
.nav-description:first-child {...}
/* NG (セレクタ階層が深い) */
nav > ul > li > a article p.nav-description:first-child {...}
#ステート
ステートルールは、レイアウトやモジュールのクラスと合わせて使います。
<div id="header" class="is-collapsed">
<form>
<div class="msg is-error">
There is an error!
</div>
<label for="searchbox" class="is-hidden">Search</label>
<input type="search" id="searchbox">
</form>
</div>
.is-collapsedはレイアウト要素が折りたたまれた状態にしたいときのためのクラスです。つまりデフォルトでは展開された状態ということです。
.is-hiddenは、もともと表示状態の要素を非表示にするためのクラスです。
##命名ルール
ステートの命名ルールは is- から始まるものとして、特定のレイアウトに依存しているものはレイアウト名を含めるようにします。
汎用的なステート ・・・ .is-hidden
特定のモジュール用ステート ・・・ .is-searchbox-hidden
上のように特定のモジュールに対してステートを作成する場合は、セレクタにモジュール名を含めるようにして、グローバルなステートと競合しないようにする必要があります。
##命名ルール
ステートは、状態を表すという点を除いてはモジュールとの違いは無さそうに思えますが、2つの重要な違いがあります。
- JavaScriptのコードと依存関係にある。
- モジュールはブラウザによるレンダリングと同時に適用されてその後は変化しない。ページ表示後にユーザーによって操作されている閒に変化するものである。
例)jQueryで状態を変更する
// bind a click handler to each searchbox
$(".btn").bind("click", function(){
// change the state to searchbox
$('.searchbox').toggleClass('is-hidden');
});
このように、たとえばボタンをクリックすると、Javascriptによってis-hiddenクラスが外されて検索ボックスがアクティブになります。
##ステートは!importantを利用してもいい
ステートは複雑なルールセットのスタイルを上書きする必要があるため!importantの使用が許可されています。SMACSSは!importantを利用することは基本的に使用を禁止しており、あくまでステートは例外です。
#テーマ
テーマはウェブサイトに見た目に影響する色や画像を定義します。テーマをほかのカテゴリから分離して独自のスタイルセットにすることで、別のテーマに切り替えたときに簡単に再定義することができます。
例えばデフォルトのリンクカラーのようなベーススタイルをオーバーライドできます。要素やモジュールの色や罫線などを変更することもあります。また、ステートの挙動も変えることもあります。
##モジュールとテーマの役割
たとえば、罫線が青色のデザインがあるとします。罫線を引くこと自体はあらかじめモジュールで定義しておき、後からテーマによって色を定義します。
例)モジュールとテーマそれぞれのルールセット
// in module-name.css
.mod {
border: 1px solid;
}
// in theme.css
.mod {
border-color: blue;
}
##HTML構造に依存しない
テーマに関してもレイアウトやモジュールと同じくシンプルなセレクタを目指します。
特定のHTMLコードの構成に依存してしまうと、その構成が少しでも変わっただけでデザインが壊れてしまいます。
また、汎用的でないセレクタも使わないようにしましょう。たとえばul.nav{} よりも .nav{} の方が再利用しやすいです。
例)ダメなセレクタ
過度に依存したセレクタ:nav > ul > li > a article p:first-child
汎用的でないセレクタ:a.button, ul.nav
##テーマでの !important の扱い
ベース、レイアウト、モジュールで !important を使うことは禁止されています。
ただし、CSS コーディング スタンダード上では「テーマでの利用は控えてください」というやんわりとしたニュアンスで書かれているので禁止ではないようです。
コントリビュートモジュールのスタイルをオーバーライドするときなどどうしても使わざるをえないときだけ !important を使用しましょう。
##タイポグラフィ
.font-largeのようなフォントクラスを定義する必要はありません。サイトにはせいぜい3〜6種類のフォントサイズしかないはずです。プロジェクトで6つ以上のフォントサイズが宣言されている場合、ユーザーは気付かず、サイトの管理をより困難にしています。
多言語化サイトの場合は、その分だけフォントを再定義する必要がありますが、そのときはロケールごとにファイルを分けてその中で定義します。
フォントの種類とサイズも見た目に影響するのでテーマの領域と思いがちですが、フォントのスタイル定義はベースにて行いましょう。
#実際のファイル構成
SMACSSの5つのカテゴリの思想を、実際のファイル構成にどう反映していくかを考えてみます。
Drupalではメジャーなテーマの1つであるOMEGAがSMACSSの思想を取り入れているので見てましょう。
##OMEGA4「Ohm」テーマの場合
##OMEGA5テーマの場合
OMEGA4,5テーマともにSMACSSの概念的なカテゴリ分けをそのままフォルダ構成に反映している訳ではないようです。OMEGA以外のSMACSS思想を取り入れたテーマをいくつか見てみてもやはりそれぞれ独自のファイル構成になっていました。
ちなみにOMEGAテーマはデザインがまっさらな状態のためテーマに相当するものは見当たりません。
##プロダクトのためのテーマカスタマイズ
OMEGAテーマなどを継承してカスタムテーマを開発するときは、コントリビュートテーマのようにシンプルなファイル構成を維持するのは難しいかも知れません。
たとえばプロジェクトでコントリビュートモジュールが吐き出すCSSをオーバーライドしたり、デザインデータを開いてみたら、コーナーごとに異なるデザインであったということもよくあります。
###規模が大きなプロダクトへのSMACSS導入
大規模サイトや多くのデザインパターンが用意されているサイトではSMACSSを導入するのは工夫が必要かもしれません。
ベース、レイアウト、モジュール、ステートはSMACSSの思想を守りつつ、テーマはコーナーごと、ページごとにCSSファイルを用意した上でほかのページにテーマCSSが干渉しないように preprocess_page フックなどで動的にテーマCSSをロードする方法は良いかもしれません。
例)プロダクトのためのSCSSファイル構成
|-base <--- ベース
| |-_normalize.scss
| |-_button.scss
| |-_forms.scss
| |-_table.scss
| |-_typography.scss
|-layout <--- レイアウト
| |-_front.scss
| |-_2nd.scss
|-component <--- モジュール、ステート
| |-_search.scss
| |-_tabs.scss
| |-_tabs.scss
| |-_views.scss
| |-_state.scss
|-design <--- テーマ (コーナーごとに直接ロードするので配下のファイルは先頭にハイフンを付けない)
| |-user.scss
| |-front.scss
| |-news.scss
| |-event.scss
|-utility <--- mixin や extend などのユーティリティ
| |-_mixins.scss <--- 汎用的に使えるmixinまとめ
| |-_variables.scss <--- 変数一元管理
| |-_abstractions.scss <--- 汎用的に使えるclassまとめ
example.style.scss <--- base、component、utilityはこの全ページ共通scss上でロードする
example.style.front.scss <--- トップページでのみ利用するlayout、designをロードする
example.style.2nd.scss <--- 2ndページで利用するlayout、designをロードする
example.style.loggedin.scss
example.style.anonymous.scss
SMACSSの概念的なカテゴライズになるべく近づくようなフォルダ構成をしています。
小規模サイトならフォルダ分けをせずに平置きでも大丈夫だと思いますが、今後拡張していくことを想定するならば、始めからカテゴリごとにフォルダ分けをしておいた方が管理しやすいと思います。
#SMACSS と BEM を併用する
BEMは、Block、Element、Modifierの頭文字を取ったものであり、ロシアのYandexが考案したフロントエンドのための命名規則です。
- Block ・・・ コンポーネントなどのコンテンツ項目(検索ボックスやログインボックスなど)
- Element ・・・ Blockの子要素
- Modifier ・・・ BlockまたはElementのステート(状態)ここでのステートはSMACSSのステートとは少しニュアンスが異なり、jsに依存しているものを差すわけではありません。
そのBEMをCSS用に最適化した MindBEMding というものがあります。
BEMの命名規則は次のパターンの通りです。
.block {}
.block__element {}
.block--modifier {}
.block__element--modifier {}
単一のハイフンとアンダースコアではなくダブルの理由は、ブロックとの区切りを明確にするためです。
そしてDrupal8のCSS コーディング スタンダードの CSS architecture の章ではBEMの名前は出されていませんが、明らかにBEMのルールと思われるものをアイデアの1つとして紹介しています。
##SMACSSの落とし穴
モジュールのルールセットを定義するとき、一般的なクラス名を付けることは再利用可能なものに見えますが、逆効果になることがあります。
例)一般的なクラス名を使ったコンポーネント
/* Button module */
.button {
/* styles */
}
/* widget module */
.widget {}
.widget .title {}
.widget .content {}
.widget .button {}
<div class="widget">
<label class="title">widget title</label>
<div class="content">widget body</div>
<button class="button" href="contents01.html">show details</button>
</div>
このような親コンポーネントの子孫セレクタにあまりにも一般的なクラスを定義してしまうと、ほかのモジュールの影響を受けてオーバーライドのためのCSSを書くことになりかねません。
###BEMルールでクラス名を付ける
ほかのモジュールの影響を受けづらくするにはBEMルールでクラス名を命名します。
例)BEMルールのクラス名を使ったコンポーネント
/* Button element class */
.widget__button { }
/* Button modifier class */
.button--info { }
<!-- Button variant is created by applying both component and element and modifier classes -->
<button class="button widget__button button--info">show details</button>
クラスがどこで使われれてどの範囲に影響するのかが明確になります。
###SMACSSとBEMを統合するのは難しい
ただ、SMACSSとBEMを完全に統合するのは難しいでしょう。
お互いのセレクタの設計に対する考え方が違っているからです。
1つの案として、ベース、レイアウト、モジュール、ステートは基本的にSMACSSのルールを採用して、テーマのレベルでBEMを採用して視覚的な部分を調整するのが良いと思います。
#Drupalコアが出力する不必要なCSSを除去する
実際のテーマ開発においては、Drupalコアやコントリビュートモジュールが出力するCSSは弊害になることがあります。
SMACSS設計の妨げになる場合は、それらのCSSプロパティをオーバーライドするという作業を行わなければなりません。
Drupalが出力する不要なCSSを削除すれば、ムダな作業を減らすことができます。
###不必要なCSSを除去する方法
####for Drupal7
Drupal7のときは、css_alterフックの中でロードしたくないCSSをunsetしていました。
/**
* Implements hook_css_alter().
*/
function hook_css_alter(&$css) {
if (!path_is_admin(current_path())) {
$css_path = drupal_get_path('module', 'node') . "/node.css";
if (isset($css[$css_path])) {
unset($css[$css_path]);
}
$css_path = drupal_get_path('module', 'user') . "/user.css";
if (isset($css[$css_path])) {
unset($css[$css_path]);
}
}
}
####for Drupal8
Drupal8からはPHPコードに頼らなくてもYAMLファイル上で、CSSファイルをオーバーライドしたり、削除できるようになりました。
# Replace an asset with another.
subtheme/library:
css:
theme:
css/layout.css: css/my-layout.css
# Remove an asset.
drupal/dialog:
css:
theme:
dialog.theme.css: false
# Remove an entire library.
core/modernizr: false
詳しくはdrupal.orgのページを参照ください。
New stylesheets-override and stylesheets-remove theme .info.yml file properties