📑

【脱!important】保守性を意識したスタイルを確実に上書きするためのテクニック

2020/10/02に公開
3

TAK(@tak_dcxi)です。今回もCSS設計に関する投稿です。

皆様はWebサイトの運用でCSSを更新・改修する際に、既存のスタイルが上書きできなくて苦しんだことはありませんか?

class名のタイポミスだったり、そもそも指定したセレクタに対応する要素が無い…といった凡ミスも原因だったりはしますが、大抵の場合他のスタイル指定が強くて上書きできない、つまり「詳細度」が影響していることが多いです。

CSSを破綻させる原因の一つの「詳細度」とは?

MDNの説明を引用すると以下の通り。

詳細度は、どの CSS プロパティが要素に最も関係があるか、すなわち適用されるかをブラウザーが決定する手段です。詳細度は様々な組み合わせの CSS セレクターで構成される一致規則に基づいています。

CSSのスタイル指定が競合した時に優先される、セレクタが生まれながらに持つ「強さ」みたいなものです。

全称セレクタ( * )が一番弱く、idセレクタ( #hoge )が一番強いです。idセレクタの持つスタイルを上書きする方法としてHTMLのstyle属性、そして後述する !important があります。

各セレクタが持つ詳細度を考慮せずに好きなセレクタ指定をしてCSSを書いた場合、大抵の場合は途中でスタイルが当たらない罠に嵌ることとなり、試行錯誤の上書き大戦争に陥って最終的には破綻します。詳細度はCSSを破綻させる最もメジャーな原因であることは間違いありません。

詳細度の罠に嵌る事例

例えば以下のようなinput要素の場合、実際に適応される背景色はredblueどちらでしょうか?

<input type="submit" class="submit-button" value="送信する">
input[type="submit"] {
  background-color: red;
}

.submit-button {
  background-color: blue;
}

正解は「red」です。これは詳細度の計算によって決定されます。

Specificity Calculatorで計算した結果ですが、.submit-buttonの詳細度0.1.0に対し、input[type="submit"]の詳細度はタイプセレクタの持つ詳細度0.0.1に属性セレクタの持つ詳細度0.1.0が加わった関係で、1ポイントの差でクラス指定が負けているからです。

こういった詳細度の罠はしっかりとしたCSS設計がなされていない場合はより顕著に現れます。

<body class="top-page">
 ...
  <p class="large-text">このテキストを上書きしたい</p>
 ...
</body>
.top-page p {
  font-size: 1rem;
}

.large-text {
  font-size: 1.5rem; /* 上のスタイル指定の方が強くて .large-text だけでは上書きできない😩 */
}

先日の投稿で「詳細度を均一化するだけでも保守性の向上に貢献する」と述べましたがまさにこれで、idセレクタやタイプセレクタへのスタイル指定を避けて各セレクタの詳細度を0.1.0になるようにキープして、CSSの書き順に気をつければこういった罠を回避できるからという理由です。

liとかdtとか一見クラス指定する必要がなさそうな要素にもclassを付与してクラスセレクタにスタイルを当てたほうが良いと言っている理由は、将来イレギュラーなスタイルをそれらの要素のうちの一部に指定することになったとしても容易に上書きを実現するためです。

ただし、このような罠はしっかりとした設計がなされている場合でも現れる可能性はあります。例えばsanitize.cssというリセットCSSを導入した場合、sanitize.cssでは以下のようなスタイル指定がなされています。

abbr[title] {
  text-decoration: underline;
}

svg:not([fill]) {
  fill: currentColor;
}

abbrに関しては利用頻度が低いので放っておいても問題なさそうですが、問題はsvgのほうで、svg要素にfill属性が含まれていない場合にクラスセレクタ1つだけでは塗りつぶし色の指定ができなくなります。

.icon {
  fill: #fff; /* これだけでは svg:not([fill]) を上書きできない😩 */
}

こういった場合に既存のCSSを編集して詳細度の調整を行うことはそのルールの影響範囲の完璧な把握がされていない限り極めて危険です

なるべく新規で追加するセレクタ指定で上書きを行いたい…そこでやりがちなのが !important です。

!importantは何故危険なのか

端的に言えば !important は最強で、!importantを打ち消すには!importantを使うしかなくなり、!important地獄になりかねないからです。

!important地獄になったらもう最後。どうにもならなくなります。複雑なスタイル指定をせざるを得なくなり、保守性の欠片もないコードになるでしょう。JavaScriptでのスタイル上書きも困難になって動作に支障をきたす可能性すらあります。そのCSS、いやそのWebサイトはもう…おしまいです。

!importantが全面的に危険だとは言いません。所謂ユーティリティークラスを利用する場合は確実なスタイルの適用を行うためにも!importantの利用が好ましいでしょう。また、とんでもないスタイル指定が成されていてどうしようもない場合は仕方なく!importantを使うしかない場合もあるでしょう。酒と同じで!importantそのものが悪いのではなく、それを扱う人間に問題があるのだと認識しましょう。

詳細度でなんとかできる場合には!importantの利用はなるべく自重したほうがいいでしょう。クレジットカードのリボ払いと同じで、何気なく手を出した!importantが将来を大きく狂わせることはよくあることなので、取り扱いには慎重になったほうがいいのです。

!importantを使わず、かつ既存のCSSを弄らずに確実にスタイルを上書きする方法とは?

ここからが本題です。!importantを使わず、かつ既存のCSSを弄らずに確実にスタイルを上書きする方法が使えるのならそれを使いましょう。ここではいくつかの方法を僕の独断と偏見で「オススメできないやり方」と「オススメのやり方」の2パターンに分けて紹介します。

サンプルは冒頭で紹介したものを再利用したものです。

<body class="top-page">
 ...
  <p class="large-text">このテキストを上書きしたい</p>
 ...
</body>
.top-page p {
  font-size: 1rem;
}

.top-page p の詳細度は0.1.1です

オススメできないやり方

1. 親要素や祖先要素に依存させる

.top-page .large-text {
  font-size: 1.5rem;
}

.top-page .large-text の詳細度は0.2.0です

このように親要素or祖先要素に依存させたセレクタ指定なら確実に上書きはできますが、そのスタイルを維持するために構造そのものも維持し続ける必要がありますし、コンポーネント自体の再利用にも難があります。保守性や汎用性の観点から避けたほうがよいでしょう。

2. タイプセレクタを付与する

p.large-text {
  font-size: 1.5rem;
}

p.large-text の詳細度は0.1.1です

.large-textがp要素でのみ使われる保証はあるのでしょうか?span要素やa要素にも将来的に利用する可能性がありますし、何よりHTML構造に依存してます。パターン1と同じ理由でオススメできません。

3. body要素に依存させる

body .large-text {
  font-size: 1.5rem;
}

body .large-text の詳細度は0.1.1です

天変地異が起こらない限りbody要素の外にコンポーネントが飛び出ることはないです。けど何か気に食わない。

オススメのやり方

1. 同一クラスを連結させる

.large-text.large-text {
  font-size: 1.5rem;
}

.large-text.large-text の詳細度は0.2.0です

おすすめパターンその1。

.hoge.hogeのように同じclassを連結して指定することで、そのclassのみを使いつつ詳細度を跳ね上げることが可能です。他のclassに影響されることなく、安全に上書きができますね。

このパターンのメリットは連結数を増やせば増やすほどその分だけ詳細度を上げることができること。つまり、.hoge.hoge.hoge.hogeと4つ連結させた場合は詳細度は0.4.0になり、上書き先の詳細度が0.2.1以上の場合にも容易に対応が可能です。ただし、上書き先にidセレクタが絡む場合には1億個繋げようとも上書きすることはできません。

.large-text.large-text.large-text.large-text の詳細度は0.4.0です

難点はセレクタが奇妙な感じになること。ただ、先に挙げた方法や!importantを使うよりも大分マシでしょう。

2. [class]を付与して「class属性を持っている○○」という指定をする

.large-text[class] {
  font-size: 1.5rem;
}

.large-text[class] の詳細度は0.2.0です

おすすめパターンその2。

.hoge[class]のように「class属性を持っている.hoge」と指定することで属性セレクタの0.1.0だけ詳細度を上げることが可能です。

class属性で指定している以上、class属性を持っていないはずが無いので常に有効になり、保守性や拡張性を保ったまま安全に上書きできます。

3. :not(:root)を付与して「ルート要素ではない○○」という指定をする

.large-text:not(:root) {
  font-size: 1.5rem;
}

.large-text:not(:root) の詳細度は0.2.0です

おすすめパターンその3。

.hoge:not(:root)のように「ルート要素ではない.hoge」と指定することで擬似クラスの0.1.0だけ詳細度を上げることが可能です。否定擬似クラスルート擬似クラスの説明についてはリンク先に委ねます。

html.hoge:not(:root)のようにhtml要素に対する上書きは矛盾となるので不可ですが、それ以外は有効になります。

このパターンの優位点は何らかの事情でclass属性を使えずにタイプセレクタで詳細度の高い指定を上書きしなければならない時でも利用できること。「何らかの事情」の具体例が思い浮かばないので思いついた方はDiscussionで教えて下さい(投げやり)


以上です。おすすめパターンを3つほど紹介しましたが、基本的にはパターン1で事足りると思います。

懸念点としては3パターンとも何も知らない人が見たらギョっとするかもしれないセレクタだということ。どうしてこういった指定をすることになったのかコメントを残しておくと親切でしょう。それかこの記事を見ろと言わんばかりにコメントに当記事のURLを貼っておくのもおすすめです(宣伝)

idセレクタが絡むときの上書き方法は?

ここまでお読みいただいた皆様はお気づきでしょうが、先程挙げたおすすめ3パターンはid属性が絡む場合には対応できません。

つまり、以下のような場合には別の方法を模索する必要があります。

<body id="top-page">
 ...
  <p class="large-text">このテキストを上書きしたい</p>
 ...
</body>
#top-page p {
  font-size: 1rem;
}

#top-page pの詳細度は1.0.1です

「これだからidセレクタにはスタイル指定するな…!」と文句を言いたいところですが、既に使われていたら嘆いていても仕方がありません。どうにかして!importantを使わずにスタイルを上書きしてみましょう。

とは言っても構造に依存せず、かつclassを使って上書きする方法は以下のやり方しか思いつきませんが…。

.large-text:not(#override) {
  font-size: 1.5rem;
}

.large-text:not(#override)の詳細度は1.1.0です

否定擬似クラスの中に指定する要素で使われていない適当なidセレクタを含める。そうすることで詳細度がid属性の1.0.0だけ加算されます。他に良い方法があればDiscussionで教えて下さい(投げやり)

【追記】フォロワーのにゃんこさんより、アンダースコアだけのid属性を否定擬似クラスを含めるのはどうかというご意見をいただきました。

.large-text:not(#_) {
  font-size: 1.5rem;
}

確かに。ランダムで適当なid属性を含めるよりも簡潔でスマートな方法ですね!

まとめ

!importantを使わず、かつ既存のCSSを弄らずに確実にスタイルを上書きする方法をいくつか紹介しました。ややハック感があるかも知れませんが、そこは気にしないでください…。

結局のところ、構築段階で更新や改修を意識したCSS設計が成されていれば (そしてベースのCSSで詳細度がやや強い指定が成されていなければ)そこまで多用されるテクニックでは無いと思いますし、これらのテクニックを使わずとも上書きができるのなら使わない選択肢を選ぶべきです。

この記事を読んでくださっている皆様が、この先ここで挙げたテクニックをなるべく使わないことを切に願うものである

余談

何らかの事情でidにスタイルを指定する必要がある時は、idセレクタを使うのではなく属性セレクタに変換して指定をすれば詳細度を0.1.0に保てます。

[id="hoge"] {
  /* おわり */
}

Discussion

catnosecatnose

同一クラスを連結させる方法(.large-text.large-text)はじめて知りました。なぜ今まで気づかなかったのか…。

TAKTAK

Zenn運営者からのコメントありがたいです…!
確かに、同一クラスを連結させる方法なんて普段考えもしませんよね。僕も感動を覚えました。

メイハクメイハク

初めまして。
いつも有益な情報の発信、勝手ながら参考にしており、非常に感謝申し上げます。

唐突ではありますが、
【同一クラスの連結でclass列の詳細度を上げる】のと似た方法について、

ハック的なやり方かもしれませんが
『1の倍数にあたるもの→すべての要素』
の特性を活かし

:nth-child(n)
を使う(場合によってはこの擬似クラスを連結させる)という方法を思いついた(発見した)のですが
こちら是非ご意見をお伺いしたく
メッセージを差し上げました次第です。

もしよろしければお手隙の際に何卒ご返信等いただけますと、ありがたく存じます。

【特徴】
(1).large-text:nth-child(n)
これはclass列+2の詳細度を有する

さらに
.large-text:nth-child(n):nth-child(n):nth-child(n)...
と連結させることで、同一クラスの連結時と同様にclass列の詳細度の無限向上が可能。

(2)
①.hoge > *
②.hoge > *:nth-child(n)
上記の2つはセレクタの意味としては同じだが、
②は①に比べclass列+1の詳細度を持つ

→ かなり限定的な状況ではあるものの、
・親、先祖に依存するというデメリット(多少の入れ子構造)を許容する(もともとがそのような設計で書かれたコード)
・何らかの理由で子要素(子孫要素)に直接クラス名を付けられない
以上のような場合、*:nth-child(n)を利用することで、該当する要素の詳細度を上げてスタイルの上書きが可能……かもしれない?
なお、必要に応じて(1)の要領で
*:nth-child(n):nth-child(n)...で更に詳細度アップも可能。

(3)書き方については、nth-child(n) 以外に
nth-child → nth-of-type
n → 1n
でも(おそらく)対応可能

(参考)
https://developer.mozilla.org/ja/docs/Web/CSS/:nth-child