CSSの詳細度(Specificity)が引き起こすトラブルとその解決策について

公開:2020-01-31 06:24
更新:2020-02-15 04:37
カテゴリ:Webサイトリニューアル,CSS

はじめに

SVGのプレゼンテーション属性を生かすためのCSS設定を行ったところ、他の要素のスタイルが変化してしまった。その原因を探ったところ詳細度(Specificity)が変化したためだということがわかった。この記事ではトラブルの内容・原因究明について述べる。

トラブルの内容

前回で、SVGのtext/tspan要素に設定したフォントに関するプレゼンテーション属性を生かすため、*セレクタを:not (text,tspan)に変更した。

/* 変更前 */
  * {
    font-family: var(--default-font);
    font-size: 18px;
    box-sizing: border-box;
    line-height: 1.85rem;
    letter-spacing:0.1rem;
    word-wrap: break-word;
    word-break: loose;
    /*border: 1px dashed gray;*/
  }
/* 変更後 */
  :not(text,tspan) {
    font-family: var(--default-font);
    font-size: 18px;
    box-sizing: border-box;
    line-height: 1.85rem;
    letter-spacing:0.1rem;
    word-wrap: break-word;
    word-break: loose;
    /*border: 1px dashed gray;*/
  }

これによってインラインSVGの<text>および<tspan>のスタイル定義が消滅し、プレゼンテーション属性で指定したフォントに関するスタイルが有効となった。

<text x="102.45901" y="784.32941" fill="#000000" font-family="Meiryo" letter-spacing="0px" word-spacing="0px" style="line-height:0%" space="preserve"><tspan x="102.45901" y="784.32941" font-size="10px" style="line-height:1.25">セッション</tspan></text>

しかし、この設定を行ったところ<h1>要素に設定したfont-sizeおよびline-heightが無効となり、:not(text,tspan)で設定したフォントサイズ・装飾に変わってしまった。

ちなみに<h1>要素の定義は以下の通りであり、:not(text,tspan)の定義よりも後に設定している。

  h1 { font-size:1.55rem;line-height: 2.25rem;}

これが問題の概要である。

CSSの詳細度とは

原因について言及するまえに詳細度についておさらいをしておく。

CSSの詳細度とは、ある要素に適合するセレクタが複数ある場合、もっとも適用すべきスタイルかを判断するための値である。この値は以下のようなフォーマットとなっている。

 (id)-(class/property selector/pseudo class)-(element)

これは各要素・属性に関する設定が存在するかどうかと、その個数によって数値化される。これの大小関係によって有効となるスタイルが決定されるわけである。詳細度が同じ場合は後で定義されたものが有効となる。

例えば、以下のcss定義を考える。

h1.aaa {font-size:20px;} /* (A) */
h1 {font-size:10px;} /* (B) */

(A)の詳細度は要素が1つとクラスが1つの組み合わせで定義されているため詳細度は0-1-1となる。 (B)の詳細度は要素が1つ定義されているため詳細度は0-0-1となる。

-区切りのそれぞれの各桁の数値を右から左にむかって比較し、大きいものがスタイルとして有効化されるわけである。

上記CSS設定では以下のHTML要素はすべて適合するが、有効になるのは詳細度が一番大きい(A)である。

<h1 class="aaa">TEST</h1>

スタイル定義にさらにID属性に関するスタイル定義を追加する。

h1.aaa {font-size:20px;} /* (A) */
h1 {font-size:10px;} /* (B) */
h1#bbb {font-size:30px;} /* (C) */

(C)の詳細度は要素が1つ、IDが1つなので1-0-1となる。

この状態で以下のHTML要素にはどのスタイルが適用されるだろうか?

<h1 class="aaa" id="bbb">TEST</h1>

答えは(C)である。スタイル定義(A)(B)(C)はこのHTML要素にすべて適合するが、詳細度がいちばん大きい(C)が適用される。

インラインスタイルの例外

今度はHTML要素にインラインスタイルを定義する

<h1 class="aaa" id="bbb" style="font-size:8px"> TEST</h1>

インラインスタイルが定義されている場合、詳細度は無視されインラインスタイルて定義された属性が有効となる。

!importantの例外

先ほどのスタイル定義の(B)に!importantを加える。するとどうなるだろうか。

h1.aaa {font-size:20px;} /* (A) */
h1 {font-size:10px !important;} /* (B) */
h1#bbb {font-size:30px;} /* (C) */

!importantが定義された場合、インライン・スタイルや詳細度よりも優先され、(B)が有効となる。

原因

:not(セレクタ・リスト)はまだブラウザではサポートされていない。CSS Level 4でサポートする予定であり、CSS Level 3では単体セレクタのみサポートとなっている。よって現状の大半ブラウザではサポートしていない。postcss-preset-envは:not(セレクタリスト)をポリフィルに置換する。今回の:not(text,tspan):not(text):not(tspan)に置換される。この置換によってセレクタの詳細度が0-0-2になり、要素セレクタh1の詳細度0-0-1より大きくなってしまう。よって<h1>要素はh1ではなく:not(text):not(tspan)で定義したスタイルが有効になってしまう。

解決策を探る

以下の簡略化されたスタイルシートを持つHTMLページを作成して解決策を探る。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>CSS詳細度テスト</title>
  <style>
    /* :not(text,tspan) のPolyfill*/
    :not(text):not(tspan) {
      font-size: 10px;
      color: red;
    }

    h1 {
      font-size: 20px;
      color: blue;
    }

    div {
      color: green;
    }

    svg {
      border: 1px solid black;
    }
  </style>
</head>

<body>
  <h1>テストタイトル</h1>
  <div>テスト</div>
  <p>テスト</p>
  <svg width="400" height="300">
    <text x="20" y="50" font-family="Meiryo">
      <tspan font-size="35pt">Hello, </tspan>
      <tspan font-size="20pt">out there</tspan>
    </text>
  </svg>
</body>
</html>

この結果はこうなる。svgのスタイル設定は望む通りの結果となっているものの、他の要素はすべて:not(text):not(tspan)のスタイルが有効となってしまっている。

が、本来はこうなってほしいのである。

試しに、h1の詳細度をあげてみる。

  html h1 {
    font-size: 20px;
    color: blue;
  }

html h1の詳細度は0-0-2となり、:not(text):not(tspan)の詳細度と等しくなる。結果、「後の定義のルールが有効となる」ルールによってhtml h1が有効化される。

がこの解決案には問題があって、除外条件:not(text):not(tspan)にさらに別の除外条件が追加されるとたちまち元の障害に戻ってしまう。よってこの案はよろしくない。

html要素に置き換えてみる

CSSにはスタイルの決定は詳細度・定義順・継承関係の3つの要因で決定される。htmlによる要素にスタイルを定義するとその内側のタグに継承されるので意図したスタイルになるのではないかという推測である。

/* :not(tspan):not(text) */
html {
  font-size: 10px;
  color: red;
}

これは期待した結果となる。

継承関係の優先順位はスタイル定義よりも低いので、svgのテキストもスタイル定義したもの通りになっている。今回の解決策としてはこれがよさそうである。:root疑似クラスでも同様の結果が得られるが、詳細度が高くなるというデメリットがあるので今回は使用しないことにする。

実際に適用してみる

実際のスタイルに適用してみると、また新たな問題が発生した。

総称セレクタでのスタイル定義は、すべての要素に対して詳細度0で属性が定義される。しかし継承はそもそもデフォルトでは継承されないプロパティが存在するので、その部分のスタイルが有効にならないのである。この問題についてはすべてに適用したいが、継承されない属性については総称セレクタで定義し、影響が出ればスタイルを上書きするしかないようである。

おわりに

今回検討した解決策で当面行こうと思うが、根本的な解決は:not()がセレクタ・リストをサポートするのを待つしかなさそうである。