本当にCSSの詳細度は必要なのか

(訳注:2016/01/05、頂いたフィードバックをもとに記事を修正いたしました。)

まず、初めに言っておきたいことがあります。この記事は、私がどれほど詳細度を嫌っているのかを延々と書いたものではないということです。もしそのような内容の記事を読みたいのであれば、インターネットで探してください。たくさんあるはずです。

この記事の目的はWeb開発コミュニティにただ質問を投げかけることですので、その答えを考えてもらえれば嬉しいです。

問題の核心を突くために、質問を言い換えてみましょう。「カスケードに詳細度が追加されていない世界の方が良かったのでしょうか、悪かったのでしょうか。

もちろん、この質問を見て、どうでも良いでしょうとか、詳細度は存在するのだから我慢するしかない、考える必要はないだろうと思う人がいることでしょう。

そう思っている人に、このような考えは正しくないということをお知らせできるのを嬉しく思います。:-)

この記事では、カスケードへの詳細度の影響を防ぐことが可能であることをお見せします。つまり、上の質問が机上の空論ではないことを説明します。もし、詳細度の及ぼす悪い影響の方が、良い影響よりも大きい場合は、実際に対応することができます。

背景

カスケードはどういう仕組みか忘れてしまっている人のために、簡単に説明しましょう。通常のCSS規則において、カスケードは「書かれた順番・詳細度・重要度」の3つの優先順位から適応するスタイルを決めています。

書かれた順番による優先順位は当然だと思います。YルールがXルールよりも後に書かれていて、両方のルールが同じ要素に適応される場合は、Yルールの宣言が”勝利”するのは自然で直感的です。

重要度による優先順位もつじつまが合います。何かをオーバーライドする必要が出てくる場合は常にあるので、重要度をつけるという選択肢があると便利です。インラインで書かれたスタイルをオーバーライドすることができるのは重要度だけですし、場合によっては必須になります。

しかし、詳細度に関しては違うと思います。

詳細度は、直感的なものではありません。特に新人の開発者にとっては。しかも、どちらかというと意図していた結果よりも、やられたと思える結果の方が多い気がします。他のシステムや言語に同等のものがあるか定かではありません。

例えば、詳細度という指標がJavaScriptにあったとします。次のテストを実行した場合、自分のコードがどれほど予測不可能なものになってしまうか想像してみてください。

window.document.foo = 'bar';
document.foo = 'qux';

assert(window.document.foo == 'bar'); // true, WTF?

上のwindow.document.foo = 'bar'設定(1番目)の参照の方が”詳細度が高い”ため、document.foo = 'qux'設定(2番目)を無効にしてしまうようなことが発生すると、収拾がつかない上に全く管理できなくなってしまいます。このようなことは実際にCSSで発生します。

でも、詳細度は便利だよね?

スタイルの構成を詳細度に頼っているWebサイトが何千、いえ何万も存在すると思います。もし明日にでもブラウザが詳細度を無視するようなことが起きれば、Webサイトは全て破壊されてしまうでしょう。

詳細度が使用されていないと言っているのではありません。明らかに使用されています。私が言いたいのは、予測不可能であることや混乱を招くことを我慢してまで使用する価値はないということです。

私は、グローバル変数と同じくらい詳細度は便利だと思います。グローバル変数に頼ることがアンチパターンであると多くの人が考えるように、詳細度にも同じことが言えるのではないでしょうか。

さらに、CSS規則で別のCSS規則を(詳細度を使用して)オーバーライドしたのに、同時に詳細度の高い規則を後の方に書き込まなかったことは一度もなかったと思います。

つまり言い換えると、次の例のように、詳細度の高いフッターリンクの定義を詳細度の低いデフォルトリンクの前に書くようなことはないということです。

.footer a {
  color: white;
  text-decoration: none;
}

a {
  color: blue;
  text-decoration: underline;
}

XルールをYルールでオーバーライドしたい場合は、私はソースコードの後の方に書きます。以上。つまり、詳細度の矛盾がある場合、それは常に偶然の出来事だということです。

ここで私は、コードを書く順番において、前の方に書いた規則を後の方に書いた規則でオーバーライドしたい時に規則の詳細度に関係なく実行するにはどうすれば良いのか考えました。

最終的に、皆で当然と同意したパラダイムから私(やチームメンバー)が誤って離れないようにするにはどうしたら良いのでしょうか。

もし、詳細度がなかったら?

CSSに詳細度が存在しない世界を想像してみてください。その世界では、カスケードが記述順や重要度だけで優先順位を決めることになります。そのため、XルールとYルールの両方がある特定の要素に一致し、Xルールの後にYルールが書かれている場合は、Xルールの詳細度に関係なく、常にYルールが勝ちます。Xルールの宣言プロパティがYルールの宣言プロパティをオーバーライドするには重要度を使用するしかありません。

この方がより良い世界なのでしょうか。予測しやすくて維持しやすい、拡張的なコードにつながるのでしょうか。

実例

多くの人はCSSで”ステート”や”ユーティリティ”クラスを使用します。この例が、要素を非表示するためのis-hiddenクラスです。

一般的に、どの要素にもステートクラスは適用されます。大抵の場合、ステートクラスは単一のクラスセレクタに定義されているため、詳細度は低くなります。しかしながら、概念としてはオーバーライドタイプのクラスになります。つまり、要素を表示したい場合、is-hiddenクラスはつけないということです。

詳細度が存在しない場合は、ステートクラスによって他のクラスをオーバーライドするには、単純にオーバーライドするコードを後の方に入れれば良いのです。しかし、今は!importantを使用して問題を解決することができます。

正しいか分かりませんが、SMACSSステートSUITユーティリティクラスをこのようなシチュエーションで用いる場合に !importantを付けることを勧められています。日常にスタイリングの必要性を核心的オプションに頼ることが、ベストプラクティスでなければ良いと思います。

カスケードから詳細度を除く

ここからが面白いところです。単にブラウザに詳細度を無視するように指示するのは不可能ですが、特定のCSSファイルやCSSファイルのセットについて、カスケードへの詳細度の影響を防ぐことは可能です

どうやって防ぐのでしょうか? 答えは、詳細度と記述の優先順位を同じにするのです。

詳細度の最も低い規則から最も高い規則へと実行されるスタイルシートを想像してください。このようなスタイルシートでは、規則の詳細度は記述順序や規則に一致するため、詳細度を効果的に式から外すことができます。

もちろん、多くの人はCSSをこのように書きませんし、このように書くように期待したり、要求したりするのは理不尽かもしれません。

しかし、コードを書いた後にトランスパイラでCSSを更新して、全ての詳細度を昇順に並べられるとしたら? さらに重要なことですが、どのセレクタにどの要素が一致するかに関係なく実現できるとしたら?

ありがたいことに、CSSセレクタの素晴らしい癖のおかげで実現できるのです。

次のCSS規則を見てください。現在詳細度は降順(各規則の上に詳細度を[idCount].[classCount].[typeCount]表記で記載)になっています。

/* 0.2.5 */
main.content aside.sidebar ul li a { }

/* 0.1.3 */
aside.sidebar ul a { }

/* 0.1.1 */
.sidebar a { }

これらのセレクタを、一致する要素に影響を及ぼすことなく詳細度の昇順に書き換えることができます。

/* 0.2.5 */
main.content aside.sidebar ul li a { }

/* 0.3.3 */
:root:root aside.sidebar ul a { }

/* 0.4.1 */
:root:root:root .sidebar a { }

全てのHTMLドキュメントはルート要素(<html>要素)を含むため、このコードは上手く動きます。:root疑似クラスをセレクタの前に付けることが要素のマッチングを変えることはありません。

例えば、:root:root:root<html>要素に一致するように、疑似クラスを多重に付けることができるため、どのセレクタにも任意で詳細度を付けて前のセレクタよりも詳細度を高めることができます。

IDの詳細度を処理する

IDセレクタは、疑似クラスセレクタよりも詳細度が高いので、セレクタの前に:rootを付けた場合の詳細度への影響がIDの詳細度を上回ることはありません。

しかし、IDセレクタの#contentと属性セレクタの[id="content"]は同じ要素と正確に一致するので、全てのIDセレクタを属性セレクタに置き換えると、上記で説明した方法でもまだ動きます。

例えば、次のような規則を見てみましょう。

/* 1.1.5 */
main#content aside.sidebar ul li a { }

/* 0.1.3 */
aside.sidebar ul a { }

/* 0.1.1 */
.sidebar a { }

トランスパイルされると、こうなります。

/* 0.2.5 */
main[id="content"] aside.sidebar ul li a { }

/* 0.3.3 */
:root:root aside.sidebar ul a { }

/* 0.4.1 */
:root:root:root .sidebar a { }

すでにルート要素を参照したセレクタを処理する

ほとんどのセレクタは、それらを見ただけで<html>要素と一致するのかどうかを知るのは不可能です。例えば、Modernizrを使用しているサイトなら、恐らく次のようなセレクタになるでしょう。

.columns {
  display: flex;
}

.no-flexbox .columns {
  display: table;
}

どちらかのセレクタが<html>要素と一致するかもしれない可能性を説明するためには、両方の可能性を含めて、次のように書き換えなければいけません。

:root.columns,
:root .columns {
  display: flex;
}

:root.no-flexbox .columns,
:root .no-flexbox .columns {
  display: table;
}

この他の方法でも問題を完全に回避することができます。どのセレクタも<html>要素と一致することを許可しないように規定し、<body>が最も高くなるようにします。

書き換えのアルゴリズム

この記事で述べてきたようなトランスパイラを私はまだ一切知りません。もしトランスパイラを書くことにに興味がおありなら、PostCSSのプラグインで書くことをお勧めします。多くのビルドステップがAutoprefixer(これはPostCSSのプラグインです)を含んでいるので、これを加えてもビルド時間や複雑さへの影響はほとんどないでしょう。

次の基本的なアルゴリズムに従ってください。

  1. スタイルシート内の各セレクタを繰り返し処理する。
  2. 各セレクタでは、
    a. セレクタがIDセレクタを含んでいたら、ID属性セレクタと置き換える。そうでなければ、何もしない。
    b. 現在のセレクタの詳細度を(置き換えられたIDのどれかを使って)計算する。
    c. 現在のセレクタが最初のセレクタなら、何もしない。そうでなければ、その詳細度を前のセレクタの詳細度と比較する。
    d. 前のセレクタの詳細度が現在のセレクタの詳細度よりも低ければ、何もしない。そうでなければ、現在のセレクタを書き換えて、前のセレクタの詳細度よりも高いか同等にする。
    i. 現在のセレクタがhtml以外の:rootセレクタやタイプセレクタで始まっていたら、原型として:rootを先頭に付けて、セレクタを書き換える。そうでなければ、最初の部分が:rootに関連付けたものと:rootの子孫がリストに追加されるようにセレクタを書き換える。
  3. 一度全てのセレクタが書き換えられると、最後のスタイルをアウトプットする。これで昇順の詳細度になる。

潜在的な欠点

ここまで説明してきたアプローチでうまく動作しますが、少し欠点も生じます。まず1つは、CSSのファイルサイズが増加してしまうことです。しかしこれは、詳細度を低く保って主に昇順で規則を記述していれば、大きな問題にはならないでしょう。それに、”root”という単語を何度も加えていくだけなので、うまくgzip圧縮できるはずです。その差は無視できると思います。

もう1つ注意すべきことは、外部スタイルを参照することの潜在的な問題点です。CDNからbootstrap.cssを組み込んで他のスタイルでこの方法を使うと、おかしなことになる可能性があります。セレクタのセットの1つは書き換えられますが、他のセットは書き換えられないからです。

そして、正常に動作するために詳細度に頼っている場合とそうでない場合があるので、必然的に、ただbootstrap.cssをビルドに組み込むだけではいけなくなるのです。

最後に

記事を締めくくる前に、冒頭で述べたことについて、もう一度触れたいと思います。この記事は質問であって、処方箋ではありません。私自信も完全に考えがまとまっていないので、ここで挙げた方法を実際に試してはいません。

この方法が物事を大いに単純化するかどうかは疑わしいですが、私たちが詳細度にどれほど頼ってきたのかを明確にしてくれるのではないでしょうか。

詳細度に戦いを挑み続けている大規模なチームがあるなら、このような助言が彼らの役に立てれば面白いと思います。試してみた方がいらっしゃれば、どうぞお気軽に結果をお知らせください。または、フォローアップ記事を書いていただければ、リンクを張ります。

更新: (2015年11月3日)

Lea VerouTwitterで指摘してくれました。セレクタの詳細度が任意で増えてしまうので、:rootの代わりに疑似クラスの:not()を使用するというものです。

:rootを使用する利点は、(<html>を含む)どの要素にも適応できるので、セレクタを分ける必要がないことです。これを使えば、例えばnot(z)not(#z)というような、型やIDレベルの詳細度も追加できるので、常にクラスを増やす必要はなくなります。

:rootを使用する欠点は、要素と一致しないように設定されているセレクタを選択しないように注意しなければいけないことです。そうしなければ、動作しなくなってしまいます。

更新 (2015年11月14日)

David Khourshidが、”詳細度の単純さ“というタイトルの返信記事を書いてくれました。CSSは命令型言語と比較するべきではなく、優先順位に関係なく規則を記述すれば、よりうまくいくと論じています。私はこの考えに同意できませんが、ここで述べたものとは違う方法を望むなら、一読されることをお勧めします。