CSSセレクタの詳細度と!important

OOCSSやSMACSS、FLOCSSやITCSSのようなCSS設計の手法はCSSの仕様を踏まえた上で考えられています。

ですが、自分はCSSの仕様について理解がおよんでいるんだろうか?と疑問が湧いたので、CSSセレクタについて調べてみました。

CSSの基本的な構文とカスケーディング

CSSの基本的な構文はセレクタとブレース、プロパティ名と値からなっています。以下の例では.fooセレクタ{}はブレース、display:はプロパティ名、block;は値です。

#foo {
  display: block;
}

CSSの構文自体はとてもシンプルで分りやすいものですが、同じセレクタ(この場合は.foo)を続けて指定すると後から指定したものが優先されます。これがカスケーディングと呼ばれるものです。

#foo {
  display: block;
}

/* こちらが優先される */
#foo {
  display: none;
}

カスケーディングはあとから指定したセレクタの方が詳細度と呼ばれる強さが同等かそれ以上の場合に起きます。例えば以下の例では同じ要素を選択してはいますが、詳細度を弱くして指定しているので上書きされません。

/* こちらが優先される */
#foo {
  display: block;
}

[id="foo"] {
  display: none;
}

詳細度とはなにか?

詳細度(Specificity)とはセレクタの強さを決めるための仕組みのことです。

詳細度は4つの数値(0,0,0,0)からなり、左から判定されます。例えばクラスセレクタ.foo)の詳細度は0,0,1,0ですが、IDセレクタ#foo)は0,1,0,0の詳細度を持っているので、IDセレクタが優先して適応されます。

クラスセレクタはIDセレクタに詳細度で勝つことはできません。上書きしようとすると.foo #fooのように詳細度を強くする(この場合は0,1,1,0)か、同じIDセレクタを上書きしたい箇所よりあとに記述する必要があります。

結合子、擬似要素、擬似クラス

セレクタ 構文 詳細度 説明
全称セレクタ * (0, 0, 0, 0) すべての要素
要素セレクタ E (0, 0, 0, 1) E要素
構造擬似クラス :root (0, 0, 1, 0) 文書のルート要素(HTML文書ではhtml要素)にマッチ
クラスセレクタ .foo (0, 0, 1, 0) class属性がfooの要素
属性セレクタ [attr] (0, 0, 1, 0) attr属性をもつ要素
属性セレクタ [attr="value"] (0, 0, 1, 0) attr属性の値がvalueである要素
IDセレクタ #foo (0, 1, 0, 0) id属性がfooの要素

上記は基本的なセレクタの指定と詳細度をまとめたものです。しかし、.foo .barのように複数のセレクタを指定したり、擬似要素や擬似クラスを指定した場合には詳細度を考慮する必要があります。

以下は代表的な結合子、擬似要素、擬似クラスをまとめたものです。結合子は複数の要素で指定する場合、擬似要素はCSSでHTML内に要素を生成するようなイメージ、擬似クラスは選択する条件を狭めたものと考えるとよさそうです。

括弧で囲んでいないものは特定のセレクタ自身の詳細度です。例えばE > Fの場合はEFは要素セレクタなので詳細度は合計すると(0, 0, 0, 2)となりますが、>自身は詳細度を上げることはないので0, 0, 0, 0となっています。

セレクタ 構文   詳細度 説明
結合子(子孫セレクタ E F 0, 0, 0, 0 (0, 0, 0, 2) E要素の子孫要素であるすべてのF要素
結合子(子セレクタ E > F 0, 0, 0, 0 (0, 0, 0, 2) E要素の直接の子要素であるすべてのF要素
結合子(隣接セレクタ、隣接兄弟セレクタ E + F 0, 0, 0, 0 (0, 0, 0, 2) E要素の直後にあらわれるF要素
結合子(間接セレクタ、一般兄弟セレクタ E ~ F 0, 0, 0, 0 (0, 0, 0, 2) E要素の後にあらわれるF要素
否定擬似クラス E:not(x) 0, 0, 0, 0 (0, 0, 0, 2) xにマッチしないE要素
擬似要素 E::first-line 0, 0, 0, 1 (0, 0, 0, 2) E要素の実際に表示されている最初の行
擬似要素 E::first-letter 0, 0, 0, 1 (0, 0, 0, 2) E要素の最初の文字(contentプロパティを指定している::before擬似要素が生成される場合は生成される要素に適応される)
擬似要素 E::before 0, 0, 0, 1 (0, 0, 0, 2) 要素の前に最初の子要素となるインラインの擬似要素を生成する
擬似要素 E::after 0, 0, 0, 1 (0, 0, 0, 2) E要素の後に最後の子要素となるインラインの擬似要素を生成する
属性セレクタ [attr^="value"] 0, 0, 1, 0 (0, 0, 1, 0) attr属性の値がvalueで始まる(前方一致)要素
属性セレクタ [attr$="value"] 0, 0, 1, 0 (0, 0, 1, 0) attr属性の値がvalueで終わる(後方一致)要素
属性セレクタ [attr*="value"] 0, 0, 1, 0 (0, 0, 1, 0) attr属性の値がvalueを含む(部分一致)要素
属性セレクタ [attr~="value"] 0, 0, 1, 0 (0, 0, 1, 0) attr属性の値がスペースで区切られた項目リストであり、値がvalueである要素
属性セレクタ `[attr ="value"]` 0, 0, 1, 0 (0, 0, 1, 0) | attr属性の値がハイフンで区切られた項目リストであり、そのうちの値の1つが正確にvalueである要素
言語疑似クラス E:lang(C) 0, 0, 1, 0 (0, 0, 1, 1) 言語情報CをもつE要素
構造擬似クラス E:first-of-type 0, 0, 1, 0 (0, 0, 1, 1) 同じ型をもつ最初のE要素
構造擬似クラス E:last-of-type 0, 0, 1, 0 (0, 0, 1, 1) 同じ型をもつ最後のE要素
構造擬似クラス E:nth-of-type(x) 0, 0, 1, 0 (0, 0, 1, 1) 同じ型をもつx番目のE要素
構造擬似クラス E:nth-of-type(odd) 0, 0, 1, 0 (0, 0, 1, 1) 同じ型をもつ奇数番目のE要素
構造擬似クラス E:nth-of-type(even) 0, 0, 1, 0 (0, 0, 1, 1) 同じ型をもつ偶数番目のE要素
構造擬似クラス E:nth-of-type(-n+x) 0, 0, 1, 0 (0, 0, 1, 1) 同じ型をもつ最初のx番目のE要素
構造擬似クラス E:nth-of-type(n+2) 0, 0, 1, 0 (0, 0, 1, 1) 同じ型をもつ最初のE要素以外
構造擬似クラス E:nth-last-of-type(x) 0, 0, 1, 0 (0, 0, 1, 1) 同じ型をもつ最初から数えてx番目のE要素
構造擬似クラス E:empty 0, 0, 1, 0 (0, 0, 1, 1) 子要素や文字(スペースも含)をもたない空の要素(コメント構文は判定されない)
ロケーション疑似クラス E:link 0, 0, 1, 0 (0, 0, 1, 1) E要素内のすべてのリンク
ロケーション疑似クラス E:visited 0, 0, 1, 0 (0, 0, 1, 1) E要素内の訪問済みのリンク
ロケーション疑似クラス E:target 0, 0, 1, 0 (0, 0, 1, 1) ハッシュ(#)が指定されているURLが参照した識別子と一致するid属性をもつ唯一の要素
ユーザーアクション擬似クラス E:hover 0, 0, 1, 0 (0, 0, 1, 1) マウスオーバーしたE要素
ユーザーアクション擬似クラス E:active 0, 0, 1, 0 (0, 0, 1, 1) マウスを押して離すまでの間か、タブキーでフォーカスしたE要素
ユーザーアクション擬似クラス E:focus 0, 0, 1, 0 (0, 0, 1, 1) キーボードで選択するか、マウスでアクティベート(フォームの入力など)をしたE要素
インプット疑似クラス E:enabled 0, 0, 1, 0 (0, 0, 1, 1) disabled属性が指定されていない使用可能なE要素
インプット疑似クラス E:disabled 0, 0, 1, 0 (0, 0, 1, 1) disabled属性が指定されている使用不可能なE要素
インプット疑似クラス E:checked 0, 0, 1, 0 (0, 0, 1, 1) チェックかon状態にされているE要素(ラジオ、チェックボックス、オプションボタン)
インプット疑似クラス E:required 0, 0, 1, 0 (0, 0, 1, 1) required属性を指定しているE要素
インプット疑似クラス E:optional 0, 0, 1, 0 (0, 0, 1, 1) required属性が指定されていないinput要素
インプット疑似クラス E:valid 0, 0, 1, 0 (0, 0, 1, 1) type属性にしたがってバリデーションが正しくおこなわれたinput要素
クラスセレクタ E.foo 0, 0, 1, 1 (0, 0, 1, 1) class属性がfooのE要素
属性セレクタ E[attr="value"] 0, 0, 1, 1 (0, 0, 1, 1) attr属性の値がvalueであるE要素

先ほど示した詳細度のリストを確認するのが分りやすいです。

結合子(>など)は詳細度を上げることがないので無視して計算します。:not()以外の疑似クラス(:hoverなど)はクラスセレクタを1つ追加したというように考えればいいでしょう(例えばa:hovera.hover)。

!important

セレクタは今までの内容を把握しておけば大丈夫だと思いますが、CSSには!importantという規則があります。!importantを指定したプロパティは指定していないプロパティよりも優先して適応されます。

例えば以下の例では#foo.fooよりも詳細度は強いですが、!importantが指定されているプロパティは.fooが優先されます。

<div id="foo" class="foo"></div>
#foo {
  display: none;
  color: red;
}

.foo {
  display: block !important; /* 上書きできる */
  color: blue; /* 上書きできない */
}

詳細度の計算は!importantの指定されていないプロパティが終わってから、!importantの指定されているプロパティ同士で比較がされます。

まとめ

調べてみてCSS設計のベタープラクティスのメリットを再確認できました。

  • クラスセレクタをベースに指定するのはHTMLに依存せずに柔軟に指定ができて、詳細度が弱いセレクタだから。
  • !importantの使用を控えるのは2回に分けて詳細度を比較する複雑さを避けるため。
  • 結合子を積極的に利用するのは詳細度を変えずにセレクタの適応範囲を狭くすることができるから。
  • style属性を使用しないのは!importantでしか上書きできないから。
  • 詳細度をできるだけ弱く保つと、シンプルに管理をすることができる。

上書き合戦をせずに、期待通りに振る舞うCSSを書くためには仕様を知っておく必要がありますね。