🎨

className を受け取らない制約は意味がない

5 min read

こちらの記事が興味深かったので、人生初のアンサーソング?(そう呼ばれてるっぽいので…)をしたためめてみます。
CSS とコンポーネント設計に対する考察

下記は className を受け取ろうが受け取るまいが、拡張されて(壊れて)しまう残念な例です。className を受け付けておらず、一見外部から影響を受ける様にはみえません。

export const RedLoginButton = () => (
  <button className={styles.button}>ログイン</button>
);
.button {
  color: #f00;
}

このコンポーネントを次の Box に入れてみます。赤文字だったはずのコンポーネント文字色が青になってしまいました。CSS を支配するのは詳細度だということが露骨に出る例です。

export const BlueThemeBox = () => (
  <div className={styles.blueTheme}>
    <RedLoginButton />
  </div>
);
.blueTheme button {
  color: #00f;
}

これは CSS Modules だから起きている、というわけではありません。CSS in JS でも同様で、簡単に壊すことができます。CSS はどんな形になろうとも CSS の仕様から免れることはできません。

export const BlueThemeBox = styled.div`
  button {
    color: #00f;
  }
`;

再利用とバンドルサイズ

そもそもこれは「壊れた指定」でしょうか?「BlueTheme」の Box に格納しているのだから、ボタンの青は意図しているものと捉えることが自然です。「親の文脈からみた場合、正しく意図が伝搬している」とも言えます。

この例では文字色しか指定していませんが 「共通指定が膨大で、ひとつのプロパティだけ変更したい場合」 はこの性質は利用できます。 「特別な理由があって、いつものあのボタン、ここだけちょっと小さくしたい」 という要望がデザイナーさんからあがってくることがあります。その時、取り得る選択肢は次のとおりです。

  • 1.複製して新しいコンポーネントとする
  • 2.コンポーネント内部で分岐処理する
  • 3.スタイルをちょっと足す

このうち、処理を増やさず・バンドルサイズを増やさないのは「3」です。スタイルの上書きは「コンポーネント利用者である親」でのみ発生し、汎用コンポーネントを共有する複数の親では起こりえません。

ほんの少しのスタイル多様性の受け入れに「1・2」は釣り合いが取れているでしょうか? 「ここだけちょっと」が複数回発生するのであれば、コンポーネントの I/F として備えて然るべきですが、CSS だけで簡単に解決できる要件の場合、バンドルサイズに優しくないのは明らかです。

className を受け取るべき理由

バンドルサイズのメリットを省いたとしても、className は受け取るべきです。これは子が className を公開しているのではなく、命名猶予を持つ事になります。例えば親コンポーネントで.myChildNameという className を渡したとします。すると「.parentHashName__89asdf .myChildName」というような 「親が与えた子名称が成す高詳細度セレクタ」 を構築でき、このセレクタは命名した親しか持つことができません。子コンポーネントは「.childHashName__9s7fad」という子に閉じられたセレクタにおいて、子自身の指定を展開します。

この2つのセレクタ内で同じプロパティ指定が競合した場合、詳細度は「親 20 > 子 10」と無意識のうちになるため、親指定は子指定に対して勝ちます。 子変化による親リグレッションは意図的にしか起こすことが出来ない構造へ自動的になるわけです。親コンポーネントというのは「末端ユースケース」であるため、子を再修飾し export するというケースはないでしょう。末端ユースケースの上書きをより確実に行うため、詳細度観点から考察しても className は受け取るべきです。

「外部からの暗黙的な影響を受けてはならない」という観点が uhyo さんの主張かと思いますが、冒頭の様に 「Style が DOM に到達してしまう」 以上、この問題は常につきまといます。

詳細度との戦い

この詳細度のしがらみは、歴代 CSS 設計で大いに考察され、対策が練られてきました。BEM をご存知でしょうか?とても長いセレクタ名称を確保し、特定のコンポーネントがグローバルスコープで侵犯されないよう防ぐ「命名規則カプセル化」は有名です。同時に、詳細度を常に「10」に保つことで、このしがらみを無きものとしました。

CSS 指定で悪名高い!importantが過去よく使われた理由は、不明瞭な詳細度のインターセプトを受けていた事にほかなりません。「詳細度を常に 10 に保つこと」 という掟を破ったことにより!importantパッチを適用せざるを得ない状況を招いたのが原因です。

なかには詳細度を巧みに操る CSS 設計もありました。文献が少ないですが ITCSS がそれです。私自身も過去「ユーティリティクラスのみ id で詳細度を 100 足す」といった様な細工を基礎設計に施した事がありました。詳細度設計は CSS 設計で一番はじめに取り組まなければいけない考察ポイントです。(※これは、今現在も変わりません。reset.cssに含まれるセレクタは、全て詳細度「10」を下回っています)

className に hash 値が付与されたコンポーネントは、詳細度が常に「10」を約束されます。いつしか忘れ去られてしまっているこの詳細度を「飼い慣らすか・封じるか」という方針が2つありますが、コミュニティは「封じる」方がより簡単、という判断をしている様に思います。昨今!importantを見なくなった理由は、詳細度の憂慮を機械的に封じることに成功したためです(※冒頭、私は数行でそれを簡単に壊してみせましたが)

詳細度のシュミレーターなどもありますので、詳細度自体が初見の方は、どういった指定でどの程度の値が算出されるのか、試してみてください。一度理解してしまえば、末端ユースケースのコンポーネントで詳細度が高ければそれで良いという単純な話に気付けるので、詳細度はそんなに怖くなくなります。

Specificity Calculator:https://specificity.keegan.st/
MDN:https://developer.mozilla.org/ja/docs/Web/CSS/Specificity

「詳細度・DOM 概念は無視した方が楽」という選択

ルールを理解して詳細度を飼い慣らしても、脆いものに変わりはありません。設計担当者の手を離れ、全く意図しない指定を書かれてしまうのもまた、CSS の宿命です。この詳細度のしがらみをより確実に封じるためには 「Style が DOM を知っている」 という関係を覆し 「DOM が Style を知っている」 という下克上が必要です。

何の変哲もないいつもの styled.div 直下に指定を書くことで下克上が成立していることに気づきましたか?詳細度の概念も、DOM の概念もここにはありません。見通しが悪い、累積 HoC が気になるなどの意見もありますが styled.div で子指定を含まないコンポーネントは「詳細度を常に 10 に保つ」 掟を守っています。

// 「Style が DOM を知っている」
export const WellKnownParent = styled.div`
  button {
    color: #00f;
  }
`;
// 「DOM が Style を知っている」
export const Alone = styled.div`
  color: #00f;
`;

そして昨今注目されている、Tailwind CSS を筆頭としたユーテリティファーストも、DOM に直接 Style を指定するため「DOM が Style を知る」という関係です。「"関心の分離をしない"ことの方が時に重要」と Adam Wathan 氏は訴えていますが 「詳細度競合を起こさない・DOM 概念が Style に漏れない」 という、ここまで述べた憂慮も見事に払拭しています。

https://adamwathan.me/css-utility-classes-and-separation-of-concerns/
<div class="text-xl font-semibold text-gray-500">$110.00</div>

以下は公式に掲載されているため問題のない書き方ですが、もうここまで読めば、何を失っているのか(どんな脆さが生まれてしまったのか)気付けるはずです。私が Tailwind CSS をプロダクションに導入するのなら、この書き方は必要最低限にとどめる方針にすると思います。

.btn-indigo {
  @apply py-2 px-4 bg-indigo-500 text-white font-semibold rounded-lg shadow-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:ring-opacity-75;
}

最後に

CSS には特有の難しさがありますが、CSS 設計時代の先人の知恵は現代のコンポーネント設計やライブラリ背景に影響を与えています。これを機会に、CSS 設計の資料をもう一度みてみてはいかがでしょうか?私にとってより良いコンポーネント設計のヒントは、いつもそこから得たものです。

「詳細度・DOM 概念は無視した方が楽」とする事もよし、CSS 詳細度やセレクタとうまく付き合うこともよし。CSS 選定はそれぞれのバックグラウンドにあわせ、何を最優先するのかチームで議論してみてください。