CSSのセレクタは内向きか外向きかで予め分割しておくと、つらくなりにくいんじゃないかなという話

最近、CSS 周りの共通化で共通化しうるスタイルも単一のセレクタにかかれていることで同じデザインを他の箇所で使いたいと思うたびに打ち消しスタイルを使う必要があってつらいなーと感じてます

これに対する解決策は、最初にスタイリングをするタイミングで

  • 単一のセレクタで足りるとしてもあえて分割しておき、セレクタの足し算でスタイルを当てる
  • 適切な単位で 分割しておくこと
    • 内向きのプロパティと外向きのプロパティで分割する

だと思っていて、この辺りで普段意識してることを言語化して起きたい意図で書いてます

あと、この辺りのベストプラクティスに関する記事等があまり見つからず良くわからないので、とりあえず書いておいてたたき台にしたいという意図もあります

困ってる対象が古き良きモノリシックなアプリケーションなので、コンポーネントベースのスタイリングについては外します

困っていること

雑に例を用意すると、例えば こんな感じ で固定ヘッダ下に置くナビゲーションメニューをスタイリングしたとします

.nav-list {
  position: fixed;
  top: 0;
  right: 0;
  list-style: none;
  display: flex;
  justify-content: flex-end;
  padding: 0 10px;
  margin: 0 10px 0 0;
  background: lightblue;
}

あとから別の箇所でもこのナビゲーションを使いたくなった時、見た目に関しては等しくしたいので共通のセレクタを当てたいなと考えますが、固定ヘッダで使うわけではないので、明らかに postion: fixed はいらないし、margin についても周りの要素に合わせて変えたいとなるはずです

となると、別のセレクタで打ち消して対応することになります

<div class="main">
  <p>left</p>

  <!--  別の場所で再利用したくなった  -->
  <ul class="nav-list nav-list-in-main">
    <li class="nav-list-item">
      <button class="nav-list-item-butonn">Go to page1</button>
    </li>
    <li class="nav-list-item">
      <button class="nav-list-item-butonn">Go to page2</button>
    </li>
    <li class="nav-list-item">
      <button class="nav-list-item-butonn">Go to page3</button>
    </li>
  </ul>

  <p>right</p>
</div>
// 打ち消しセレクタ
.nav-list-in-main {
  position: static;
  margin: 0 20px;
  height: $-nav-list-height;
}

これで対応はできます(全体ソース)が、打ち消しスタイルだとどうしてもわかりにくくなります

上の例でも読み込み順で勝ててるだけなので、nav-list-in-main の読み込みが先に行われている(or 読み込みの順番が変わる可能性がある)なら詳細度を上げるなどの対応も必要になってきます

打ち消し前提でやっているとソースコードの規模が大きくなるにつれて、どんどん詳細度があがっていってメンテナンスがつらくなりますし、「どうしてそのスタイルがあたっているのか」を推測するのが難しくなります(頭で詳細度の計算と読み込み順をエミュレートしきれるなら別ですが w)

できる限り当てているセレクタにプロパティの重複がない状態が理想だと思います

内向きのプロパティと外向きのプロパティで予めセレクタを分割しておく

打ち消しスタイルを使わざるを得なかった原因は共通化したいセレクタに余分なプロパティが含まれていたことなので、再利用されそうなプロパティ郡ごとにセレクタを分割しておけば打ち消しスタイルを使わずに共通化ができます

じゃあ、なにを基準に分割すれば良いの?という話ですが、個人的にはとりあえず内向きのプロパティと外向きのプロパティに分割しておけば最低限上手くいくと思っています

  • 内向きのプロパティ
    • 要素の内側をどう表示するかを選択するプロパティ
    • background, color, font-size, padding, …etc
    • padding も含む
  • 外向きのプロパティ
    • 外から見てその要素をどう配置するかを選択するプロパティ
    • margin
    • position (top, left, right, bottom)
    • grid-area
    • z-index

上の例で言うなら、以下のように分割しておくことになります

.nav-list {
  display: flex;
  justify-content: flex-end;
  padding: 0 10px;

  background: lightblue;
  list-style: none;
}
.fixed-menu {
  position: fixed;
  top: 0;
  right: 0;
  margin: 0 10px 0 0;
}

「共通化したい」≒「内向きのスタイルを共通化したい」

「共通化したい」はほとんど、同じ見た目の要素を別の場所で使いたいということであり、見た目とはすなわち内向きのプロパティによって作られる表示のことです。言い換えれば、内向きのプロパティだけでまとまっていれば、ほとんど打ち消さずに共通化できるということです

他の場所で使いたくなったら、内向きのプロパティ(.nav-list) を流用して外向きのプロパティを新しく作れば良い(もちろん外向きのプロパティもたまたま共通なら流用すれば良い)ことになります

上の例なら

<!-- 固定ヘッダでの利用 -->
<ul class="nav-list fixed-menu">
  <li class="nav-list-item">
    <button class="nav-list-item-butonn">Go to page1</button>
  </li>
  <li class="nav-list-item">
    <button class="nav-list-item-butonn">Go to page2</button>
  </li>
  <li class="nav-list-item">
    <button class="nav-list-item-butonn">Go to page3</button>
  </li>
</ul>

<!-- 他の場所での利用1 -->
<ul class="nav-list outer1">
  <!-- ...省略 -->
</ul>

<!-- 他の場所での利用2 -->
<ul class="nav-list outer2">
  <!-- ...省略 -->
</ul>

全体ソース: https://codepen.io/d-kimuson/pen/ZEyYVKv

これなら打ち消されていないので読むときにわかりやすいですし、複数のセレクタで重複しているプロパティが存在しないので詳細度をあげる必要もなく健全です

逆に外向きの配置を共通化したいことも少なからずあると思っていて、そのときは逆に内向きのプロパティが邪魔になりますから、いずれにせよ内向き外向きは分けておいたほうが良いということに変わりありません

全てのセレクタで内向きと外向きのプロパティをわけるのか?

もちろんそんなことはなくて、あくまで(未来も含めて)共通化の余地があるかどうかで判断したら良いと思います

例えば、全ページ完全に共通のヘッダーを使うなら別のバリエーションのヘッダーが欲しくなることはないので必要ないでしょうし

上の例の nav-list-item はおそらく単体では使わない(nav-list の中でのみ使う)ので、共通化の単位として小さすぎて必要ないと思います

Vue やら React で作るときにコンポーネントとして分けたくなるくらいの単位なら、セレクタ分けておいたほうが良いよという程度のものです

というか、コンポーネントベースで作ってると自然と再利用を意識できるので、外向きのレイアウトをコンポーネントに持たせるというアンチパターン自体踏みにくいと思っていて、再利用が意識しにくい HTML ベタ書きだからこそ、CSS レベルで再利用を意識する必要があるのかなと思います

その他の分割基準

HTML のデフォルト値上書き

基本、内向き外向きを分ければだいたい上手くいくと思うけど、別に HTML デフォルト値の上書き用のプロパティも分けておいたほうが良いと思っています

  • セマンティクス的に li を使いたいが、箇条書きスタイルにしたくないので list-style: none;
  • a と button で共通のデザインを使うための color: inherit;, text-decoration: none;, …etc

こういうのも共通化のときに過不足になり、打ち消しスタイルを使いがちなのでユーティリティクラスにわけてしまったほうが良いと思います

僕はその辺りでいちいちセレクタ当てたくないという気持ちがあるので、reset.css を使うことが多いです

まとめ

  • CSS を書いていて、marigin やら position やら z-index やらを書きたくなったら、分割の必要性がないか(=再利用の余地がないか)を検討したほうが良い
  • 再利用の可能性が未来含めてあるのならプロパティが内向きなのか外向きなのかでセレクタを分割しよう
  • 特に margin は気軽にセットでつけてしまいがちなので気をつける
    • コンポーネントに margin を持たせるな!!