前回の記事 Angular 12 にシュッと導入してチャッと理解した tailwindcss をピャッとカスタマイズする からしばらく経過してしまいました。時が流れて手元のプロジェクトの Angular/tailwindcss ともにアップデートしてしまったのですが続きを書きます。
今回は tailwindcss でカスタムプラグインを作る方法をメインに紹介します。
記事執筆の環境は下記の通りです。
- @angular/cli v13.1
- node v16.10 (npm v7.24)
- tailwindcss v3.0
プラグインとは
tailwindcss を使ったユーティリティクラスの幅を広げるための機能です。
プラグインには、公式などのプラグインを取り込む方法と、ユーザー定義のカスタムプラグインを自作する方法との 2 通りがあります。
プラグインを取り込む方法
例えば公式のプラグイン @tailwindcss/typography
を取り込んでみましょう。
npm install -D @tailwindcss/typography
// tailwind.config.js
module.exports = {
theme: { ... },
plugins: [
require('@tailwindcss/typography'),
],
}
このプラグインでは prose
クラスが提供されています。
ビルトインのユーティリティと同じように CSS クラスとして記述します。
<article class="prose lg:prose-xl">
<h1>...</h1>
</article>
カスタムプラグインを自作する方法
次はカスタムプラグインを見てみましょう。
カスタムプラグインは、設定ファイルの plugins
に関数を渡します。
// tailwind.config.js
const plugin = require('tailwindcss/plugin');
module.exports = {
plugins: [
plugin(function({ addUtilities, addComponents, e, prefix, config }) {
...
}),
]
}
引数の addUtilities
addComponents
e
などはカスタムプラグインを作るためのユーティリティです。
必要な引数を 分割代入 で宣言して使います。
公式のプラグインや npm で公開されたプラグインはそれぞれドキュメントに使用方法が書いてあるので詳細な説明は割愛します。
ここから先はカスタムプラグインを主軸にした記事です。
addUtilities を使ってフェードインするボタンを作る
まず引数に addUtilities
を宣言する例として、不透明度10%から100%にふわっと変化するボタンを作ってみましょう。
// tailwind.config.js
const baseConfig = {
...
plugins: [
plugin(({ addUtilities }) => {
const utilities = {
'.fade-in': {
transition: 'opacity 1s ease-in-out',
opacity: '1'
}
};
addUtilities(utilities);
}),
],
名前は fade-in
としました。
適用する CSS を示すオブジェクトの記法は CSS-in-JS syntax を使います。
const utilities = {
// ユーティリティクラス名
'.fade-in': {
// 適用する CSS
transition: 'opacity 1s ease-in-out',
opacity: '1'
}
};
CSS-in-JS ではクォートまたはキャメルケースでキーを指定します。
どちらも同じようにビルドするとダッシュケースに変換されます。
// クォート指定
'background-color': 'black'
// camelCase
backgroundColor: 'black'
// ビルド後:dashCase
background-color: 'black'
fade-in
クラスはビルトインのユーティリティクラスと同じように CSS クラス名として使用します。
hover:fade-in
とする事で、マウスホバーをトリガにして変化します。
<button
class="rounded-md bg-blue-300 p-2 opacity-10 hover:fade-in"
>サンプル</button>
addComponents を使ってアラート表示を作る
次は引数に addComponents
を宣言して、アラート表示するパーツを作ってみましょう。
// tailwind.config.js
const baseConfig = {
...
plugin(({ addComponents }) => {
const components = {
'.alert': {
backgroundColor: 'rgba(150, 0, 10, 1)',
borderColor: 'rgba(255, 0, 0, 1)',
borderRadius: '8px',
color: 'rgba(255, 255, 255, 1)',
padding: '1rem',
},
};
addComponents(components);
}),
rgba()
は CSS で関数表記により色を宣言するものです。
red, green, blue を 1〜255 で範囲指定し、続けて alpha(透過度) を 0.0〜1.0 で範囲指定します。
'.alert': {
backgroundColor: 'rgba(150, 0, 10, 1)',
borderColor: 'rgba(255, 0, 0, 1)',
宣言した alert
はビルトインのユーティリティクラスと同じように CSS クラス名として使用します。
<div class="alert">エラーが発生しました</div>
addUtilities と addComponents は何が違うのか
addUtilities
addComponents
はどちらも引数で宣言し、JavaScript のオブジェクトを渡す記法も同じです。
plugins: [
plugin(({ addUtilities }) => {
const utilities = { ... };
addUtilities(utilities);
}),
],
plugins: [
plugin(({ addComponents }) => {
const components = { ... };
addComponents(components);
}),
],
tailwindcssには base
components
utilities
の「バケット」があり、これらは CSS を適用するスコープを示しています。
addUtilities
addComponents
は登録先のバケットを区別するために異なる関数が用意されたものです。
plugin(({ addUtilities, addComponents, addBase }) => {
// utilities バケットに登録
const utilities = {
'.fade-in': { ... }
};
addUtilities(utilities);
// components バケットに登録
const components = {
'.alert': { ... }
};
addComponents(components);
// base バケットに登録
const base = {
'h1': { ... }
};
addBase(base);
}),
tailwindcss のビルドコマンドを叩くと、それぞれのバケットの末尾にカスタムプラグインが出力される事がわかります。
./node_modules/.bin/tailwind build -o ./build.css
/* build.css */
/* base バケット */
textarea { ... }
input { ... }
button { ... }
/* 追加したプラグイン */
h1 { ... }
/* components バケット */
.bg-gray-100 { ... }
.bg-gray-200 { ... }
/* 追加したプラグイン */
.alert { ... }
/* utilities バケット */
.transition { ... }
/* 追加したプラグイン */
.fade-in { ... }
tailwindcss では詳細度の高い CSS が優先して適用されるように、宣言した順に関わらずバケットの出力順は常に一定となっています。
カスタムプラグインをバリアント指定できるようにする
tailwindcss では hover:
sm:
dark:
など、実行環境やユーザーアクションにより変化する状態を「バリアント」として指定します。
addUtilities
などの関数にはカスタムプラグインをバリアントと組み合わせて使うためのオプションを指定する事ができます。
// tailwind.config.js
const baseConfig = {
...
plugin(({ addUtilities }) => {
const utilities = {
'.full-screen': {
height: '100vh',
width: '100vw',
},
'.half-screen': {
height: '50vh',
width: '50vw',
},
};
// "sm:" "md:" "lg:" などレスポンシブのバリアントを有効にする
addUtilities(utilities, {
variants: ['responsive']
});
}),
],
<div class="full-screen lg:half-screen">foobar</div>
わざわざオプションでバリアントを指定するのは、部分的に有効にする事でビルドサイズを肥大させないというねらいがあるようです。
手元で試したところ variants
オプションを指定しなくとも hover:
や lg:
などのバリアントはカスタムプラグインで使う事ができたため、標準で有効になっているものがあるかもしれません。
ビルトインのユーティリティクラスを参照する
text-2xl
などビルトインのユーティリティクラスを参照する場合は、引数に theme
を宣言して使います。
// tailwind.config.js
const baseConfig = {
...
plugin(({ addUtilities, theme }) => {
'.lead': {
// "text-2xl" の外観を引用
fontSize: theme('fontSize.2xl'),
}
addUtilities(utilities);
}),
],
または config
を宣言すると同じようにビルトインのユーティリティクラスが参照できます。
// tailwind.config.js
const baseConfig = {
...
plugin(({ addUtilities, config }) => {
'.lead': {
// "text-2xl" の外観を引用
fontSize: config('theme.fontSize.2xl'),
}
addUtilities(utilities);
}),
],
theme
と config
では得られる値のフォーマットに違いがあるようです。
theme('fontSize.2xl')
> '1.5rem'
config('theme.fontSize.2xl')
> [ '1.5rem', { lineHeight: '2rem' } ]
config
のほうがより詳細な情報が得られますが、フラットなオブジェクトでないため適用にはひと手間かかりそうです。
plugin(({ addUtilities, theme, config }) => {
'.lead': (([fontSize, options]) => {
// { fontSize: '1.5rem', lineHeight: '2rem' } というオブジェクトに変換する
return { fontSize, ...options };
})(config('theme.fontSize.2xl')),
}
addUtilities(utilities);
}),
addVariant を使ってバリデーションエラーを表現するボタンを作る
addVariant
を使うと foobar:
のようなカスタムバリアントを作る事ができます。
最後に「フォームがバリデーションの要件を満たしていない時に submit ボタンの外観を変更する」というバリアントを作ってこの記事を締めくくりたいと思います。
const plugin = require('tailwindcss/plugin');
// tailwind.config.js
const baseConfig = {
...
plugins: [
plugin(({ addVariant, e }) => {
addVariant('form-invalid', ({ modifySelectors, separator }) => {
modifySelectors(({ className }) => {
return `form:invalid .form-invalid${e(separator)}${className}`;
});
});
}),
],
variants: {
extend: {
backgroundColor: ['form-invalid'],
}
},
addVariant
の最初の引数には form-invalid:
のようにバリアント名として使うテキストを指定します。
addVariant('form-invalid', ({ modifySelectors, separator }) => {
...
}
addVariant
は第二引数の関数の中で特別な引数を受け取ります。
modifySelectors
とはビルドファイルに出力する CSS を作る役割を担っています。
addVariant('form-invalid', ({ modifySelectors, separator }) => {
modifySelectors(({ className }) => {
// return "{ ビルドファイルに出力する CSS }";
});
});
HTML で form-invalid:bg-red-600
と記述した時、 separator
には :
、 className
には bg-red-600
が代入されます。
e
は plugin
関数から受け取ったもので、記号をエスケープするために呼び出します。
addVariant('form-invalid', ({ modifySelectors, separator }) => {
modifySelectors(({ className }) => {
return `form:invalid .form-invalid${e(separator)}${className}`;
});
});
background-color
のバリアントとしてカスタムバリアントを使えるように variants
に登録します。
variants: {
extend: {
backgroundColor: ['form-invalid'],
}
},
結果、ビルドファイルには以下のような CSS クラスが出力されます。
form:invalid .form-invalid\:bg-red-600 {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}
HTML にフォームを記述してみましょう。
<form>
<input type="text" required />
<button
class="rounded p-1 text-white bg-blue-600 form-invalid:bg-red-600"
type="submit"
>登録</button>
</form>
input[required]
が要件を満たさない(空欄)の時、button
には ".form-invalid\:bg-red-600"
という CSS クラスの外観が適用されます。
CSS の 擬似クラス には form-invalid
なんてものは存在しないので、:
をエスケープして擬似クラスと解釈されないようにしている訳ですね。
ビルトインのユーティリティクラスでもこのようなビルドファイルへの出力が行われていました。この仕組みを知った時 tailwindcss の設計に感嘆の声を漏らしてしまいました。すごいですね tailwindcss。考え抜かれてますね。
おわりに
今回はカスタムプラグインの記述方法に焦点を当てて記事を書きました。
カスタムプラグインが増えてもっさりしてきたら、プロジェクト内で外部プラグイン化するなど工夫してみたいと思います!