Tailwind CSS + :where() で スタイル上書き可能なコンポーネント設計
コードは、Tailwind CSS を利用する前提で例示する。
いままで
UIコンポーネントの実装において、 margin
などの外部に影響するスタイルについて、コンポーネントを呼び出す側で指定する実装をすることが多い。
import classNames from 'classnames'
export const SectionHead = ({ className, children }) => {
return <h2 className={classNames('py-2 border-b text-lg', className)}>{children}</h2>
}
const Page = () => {
return <main>
<section>
<SectionHead className="mb-2">はじめに</SectionHead>
<p>こんにちは!この記事では...</p>
</section>
</main>
}
なぜ分けるのか
SectionHead
コンポーネントで mb-2
を指定した場合、別のセクションで mb-4
を指定したい場合に、 mb-4
のスタイルが適用される保証がない。
スタイルは、あとから定義されているほうが優先される (同じ詳細度の場合)。
SPA や SSG の場合、生成時やページ読み込みタイミングによって、スタイル定義の順番が変わることがよく発生する。
<!-- こう出力されると、「おわりに」 には margin-bottom: 16px が適用される -->
<style>
.mb-2 { margin-bottom: 8px; } /* 同じ詳細度 */
.mb-4 { margin-bottom: 16px; } /* 同じ詳細度かつ定義が後なので適用 */
</style>
<!-- こう出力されると、「おわりに」 には margin-bottom: 8px が適用されてしまう -->
<style>
.mb-4 { margin-bottom: 16px; } /* 同じ詳細度 */
.mb-2 { margin-bottom: 8px; } /* 同じ詳細度かつ定義が後なので適用 */
</style>
<main>
<section>
<h2 class="py-2 border-b text-lg mb-2">はじめに</h2>
<p>こんにちは!この記事では...</p>
</section>
<section>
<h2 class="py-2 border-b text-lg mb-2 mb-4">おわりに</h2>
<p>ありがとうございました!</p>
</section>
</main>
!important
を利用するなどして詳細度を上げれば、margin-bottom: 16px
を必ず適用することはできるが、詳細度の管理が大変なのでできれば使いたくない。気にしないならこれで解決するのでどうぞ。
このような事象を未然に防ぐ方法として、スタイルが内部で収まるか外部にも影響するかによって、スタイルを付与するタイミングを変えている。
課題
例示したセクションの見出しのmarginは、シンプルなデザインであれば基本的にはどこでも同じで、変わることのほうが少ない。
基本スタイルである mb-2
を毎回記述するのも冗長だし、メンテしづらい。
const Page = () => {
return <main>
<section>
<SectionHead className="mb-2">はじめに</SectionHead>
<p>こんにちは!この記事では...</p>
</section>
<section>
<SectionHead className="mb-2">アメリカの場合</SectionHead>
<p>自由の女神が人気です</p>
</section>
<section>
<SectionHead className="mb-2">フランスの場合</SectionHead>
<p>エッフェル塔がありますね</p>
</section>
<section>
<SectionHead className="mb-2">ドイツの場合</SectionHead>
<p>ドイツは好きですよ</p>
</section>
<section>
<SectionHead className="mb-4">おわりに</SectionHead>
<p>ありがとうございました!</p>
</section>
</main>
}
解決策
:where()
擬似クラス関数を利用する (MDNでの解説)。
:where()
擬似クラス関数を利用し、確実に上書きができるデフォルトスタイルとして、コンポーネント側に margin
を定義する。
:where()
は、 :where(article, #about, .selector) h1 {}
など、カンマ区切りで渡した複数のセレクターのいずれかに当てはまれば適用されるという、OR関数のような働きをする。
同様の働きをする擬似クラス関数に :is()
もあるが、 :where()
は詳細度が 0 になるという特徴もあり、これを利用する。
<!-- こう出力されると、「おわりに」 には margin-bottom: 16px が適用される -->
<style>
:where(.mb-2) { margin-bottom: 8px; } /* 詳細度が低い */
.mb-4 { margin-bottom: 16px; } /* 詳細度が高いので適用される */
</style>
<!-- こう出力されても、「おわりに」 には margin-bottom: 16px が適用される -->
<style>
.mb-4 { margin-bottom: 16px; } /* 詳細度が高い */
:where(.mb-2) { margin-bottom: 8px; } /* 定義が後だが、詳細度が低いので適用されない */
</style>
:where()
は Chrome であれば v88 以上から使えるが、執筆時点で v106 なので、利用して全く問題ないだろう。
Tailwind CSS での実装方法
Tailwind CSS v3.2.1 時点で、:where()
を扱う機能がない。
プラグインを作成する。4行で済む。
const plugin = require('tailwindcss/plugin');
module.exports = {
content: [],
theme: {},
plugins: [
plugin(({ addVariant }) => {
addVariant('where', ':where(&)');
}),
],
};
これだけで、where:...
class で :where(...)
というセレクターが生成されるようになった。
import classNames from 'classnames'
export const SectionHead = ({ className, children }) => {
return <h2 className={classNames('py-2 border-b text-lg where:mb-2', className)}>{children}</h2>
}
const Page = () => {
return <main>
<section>
{/* 指定せずとも mb-2 が適用される */}
<h2>はじめに</h2>
<p>こんにちは!この記事では...</p>
</section>
<section>
{/* mb-4 が必ず適用される、はず */}
<h2 class="mb-4">おわりに</h2>
<p>ありがとうございました!</p>
</section>
</main>
}
Discussion