Angular 12 にシュッと導入してチャッと理解した tailwindcss のプラグインをサッと作る

前回の記事 [Angular 12 にシュッと導入してチャッと理解した tailwindcss をピャッとカスタマイズする](https://qiita.com/ringtail003/items/55fa0f681a1b73c7f2b7) からしばらく経過してしまいました。時が流れて手元のプロジェクトの Angular/tailwindcss ともにアップデートしてしまったのですが続きを書きます。

今回は tailwindcss でカスタムプラグインを作る方法をメインに紹介します。

記事執筆の環境は下記の通りです。

- @angular/cli v13.1
- node v16.10 (npm v7.24)
- tailwindcss v3.0

# プラグインとは

tailwindcss を使ったユーティリティクラスの幅を広げるための機能です。
プラグインには、公式などのプラグインを取り込む方法と、ユーザー定義のカスタムプラグインを自作する方法との 2 通りがあります。

## プラグインを取り込む方法

例えば公式のプラグイン `@tailwindcss/typography` を取り込んでみましょう。

https://tailwindcss.com/docs/typography-plugin

```sh
npm install -D @tailwindcss/typography
```

```js
// tailwind.config.js
module.exports = {
  theme: { ... },
  plugins: [
    require('@tailwindcss/typography'),
  ],
}
```

このプラグインでは `prose` クラスが提供されています。
ビルトインのユーティリティと同じように CSS クラスとして記述します。

```html
<article class="prose lg:prose-xl">
  <h1>...</h1>
</article>
```

## カスタムプラグインを自作する方法

次はカスタムプラグインを見てみましょう。
カスタムプラグインは、設定ファイルの `plugins` に関数を渡します。

https://tailwindcss.com/docs/plugins

```js
// tailwind.config.js
const plugin = require('tailwindcss/plugin');

module.exports = {
  plugins: [
    plugin(function({ addUtilities, addComponents, e, prefix, config }) {
      ...
    }),
  ]
}
```

引数の `addUtilities` `addComponents` `e` などはカスタムプラグインを作るためのユーティリティです。
必要な引数を [分割代入](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) で宣言して使います。

公式のプラグインや npm で公開されたプラグインはそれぞれドキュメントに使用方法が書いてあるので詳細な説明は割愛します。
ここから先はカスタムプラグインを主軸にした記事です。

## addUtilities を使ってフェードインするボタンを作る

まず引数に `addUtilities` を宣言する例として、不透明度10%から100%にふわっと変化するボタンを作ってみましょう。

```js
// 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](https://tailwindcss.com/docs/plugins#css-in-js-syntax) を使います。

```js
const utilities = {
  // ユーティリティクラス名
  '.fade-in': {
    // 適用する CSS
    transition: 'opacity 1s ease-in-out',
    opacity: '1'
  }
};
```

CSS-in-JS ではクォートまたはキャメルケースでキーを指定します。
どちらも同じようにビルドするとダッシュケースに変換されます。

```js
// クォート指定
'background-color': 'black'

// camelCase
backgroundColor: 'black'

// ビルド後：dashCase
background-color: 'black'
```

`fade-in` クラスはビルトインのユーティリティクラスと同じように CSS クラス名として使用します。
`hover:fade-in` とする事で、マウスホバーをトリガにして変化します。

```html
<button 
  class="rounded-md bg-blue-300 p-2 opacity-10 hover:fade-in"
>サンプル</button>
```

## addComponents を使ってアラート表示を作る

次は引数に `addComponents` を宣言して、アラート表示するパーツを作ってみましょう。

```js
// 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 で範囲指定します。

```js
'.alert': {
  backgroundColor: 'rgba(150, 0, 10, 1)',
  borderColor: 'rgba(255, 0, 0, 1)',
```

宣言した `alert` はビルトインのユーティリティクラスと同じように CSS クラス名として使用します。

```html
<div class="alert">エラーが発生しました</div>
```

## addUtilities と addComponents は何が違うのか

`addUtilities` `addComponents` はどちらも引数で宣言し、JavaScript のオブジェクトを渡す記法も同じです。

```js
plugins: [
  plugin(({ addUtilities }) => {
    const utilities = { ... };
    addUtilities(utilities);
  }),
],
```

```js
plugins: [
  plugin(({ addComponents }) => {
    const components = { ... };
    addComponents(components);
  }),
],
```

tailwindcssには `base` `components` `utilities` の「バケット」があり、これらは CSS を適用するスコープを示しています。
`addUtilities` `addComponents` は登録先のバケットを区別するために異なる関数が用意されたものです。

```js
plugin(({ addUtilities, addComponents, addBase }) => {
  // utilities バケットに登録
  const utilities = {
    '.fade-in': { ... }
  };
  addUtilities(utilities);

  // components バケットに登録
  const components = {
    '.alert': { ... }
  };
  addComponents(components);

  // base バケットに登録
  const base = {
    'h1': { ... }
  };
  addBase(base);
}),
```

tailwindcss のビルドコマンドを叩くと、それぞれのバケットの末尾にカスタムプラグインが出力される事がわかります。

```sh
./node_modules/.bin/tailwind build -o ./build.css 
```

```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` などの関数にはカスタムプラグインをバリアントと組み合わせて使うためのオプションを指定する事ができます。

```js
// 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']
    });
  }),
],
```

```html
<div class="full-screen lg:half-screen">foobar</div>
```

わざわざオプションでバリアントを指定するのは、部分的に有効にする事でビルドサイズを肥大させないというねらいがあるようです。
手元で試したところ `variants` オプションを指定しなくとも `hover:` や `lg:` などのバリアントはカスタムプラグインで使う事ができたため、標準で有効になっているものがあるかもしれません。

## ビルトインのユーティリティクラスを参照する

`text-2xl` などビルトインのユーティリティクラスを参照する場合は、引数に `theme` を宣言して使います。

```js
// tailwind.config.js
const baseConfig = {
...
  plugin(({ addUtilities, theme }) => {
    '.lead': {
      // "text-2xl" の外観を引用
      fontSize: theme('fontSize.2xl'),
    }

    addUtilities(utilities);
  }),
],
```

または `config` を宣言すると同じようにビルトインのユーティリティクラスが参照できます。

```js
// tailwind.config.js
const baseConfig = {
...
  plugin(({ addUtilities, config }) => {
    '.lead': {
      // "text-2xl" の外観を引用
      fontSize: config('theme.fontSize.2xl'),
    }

    addUtilities(utilities);
  }),
],
```

`theme` と `config` では得られる値のフォーマットに違いがあるようです。

```js
theme('fontSize.2xl')
> '1.5rem'

config('theme.fontSize.2xl')
> [ '1.5rem', { lineHeight: '2rem' } ]
```

`config` のほうがより詳細な情報が得られますが、フラットなオブジェクトでないため適用にはひと手間かかりそうです。

```js
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 ボタンの外観を変更する」というバリアントを作ってこの記事を締めくくりたいと思います。

```js
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:` のようにバリアント名として使うテキストを指定します。

```js
addVariant('form-invalid', ({ modifySelectors, separator }) => {
  ...
}
```

`addVariant` は第二引数の関数の中で特別な引数を受け取ります。
`modifySelectors` とはビルドファイルに出力する CSS を作る役割を担っています。

```js
addVariant('form-invalid', ({ modifySelectors, separator }) => {
  modifySelectors(({ className }) => {
    // return "{ ビルドファイルに出力する CSS }";
  });
});
```

HTML で `form-invalid:bg-red-600` と記述した時、 `separator` には `:` 、 `className` には `bg-red-600` が代入されます。
`e` は `plugin` 関数から受け取ったもので、記号をエスケープするために呼び出します。

```js
addVariant('form-invalid', ({ modifySelectors, separator }) => {
  modifySelectors(({ className }) => {
    return `form:invalid .form-invalid${e(separator)}${className}`;
  });
});
```

`background-color` のバリアントとしてカスタムバリアントを使えるように `variants` に登録します。

```js
variants: {
  extend: {
    backgroundColor: ['form-invalid'],
  }
},
```

結果、ビルドファイルには以下のような CSS クラスが出力されます。

```css
form:invalid .form-invalid\:bg-red-600 {
  --tw-bg-opacity: 1;
  background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}
```

HTML にフォームを記述してみましょう。

```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 の [擬似クラス](https://developer.mozilla.org/ja/docs/Web/CSS/Pseudo-classes) には `form-invalid` なんてものは存在しないので、`:` をエスケープして擬似クラスと解釈されないようにしている訳ですね。

ビルトインのユーティリティクラスでもこのようなビルドファイルへの出力が行われていました。この仕組みを知った時 tailwindcss の設計に感嘆の声を漏らしてしまいました。すごいですね tailwindcss。考え抜かれてますね。

## おわりに

今回はカスタムプラグインの記述方法に焦点を当てて記事を書きました。
カスタムプラグインが増えてもっさりしてきたら、プロジェクト内で外部プラグイン化するなど工夫してみたいと思います！