🐈

Tailwind CSSの嬉しいけど嬉しくない機能 - Just In Time Mode

2023/10/11に公開

はじめに

昨今にわかに話題となっている CSS フレームワークとしてTailwind CSSがあります。
このフレームワークを導入すると、実装者は CSS ファイルにセレクターを定義して CSS プロパティを書く必要がなくなります。
下記のサンプルのようにクラス属性に値を指定することで、CSS プロパティを別途独自で定義する必要が無くなります。

<figure class="bg-slate-100 rounded-xl p-8 dark:bg-slate-800"></figure>

クラスの付与だけで良いという特徴は、デザイナーとエンジニア側でのやり取りを以前よりも円滑にします。
デザイナー側の暗黙の了解を把握しておく必要がなく、エンジニアはクラス属性さえ見ればデザイナーが何をしたかったかを把握できるためです。
このように Tailwind CSS は従来のスタイル適用方法とは違いますが、多くのメリットをもたらします。
しかし、今回実装するにあたってそのメリットが不都合をもたらしました。
具体的には Just In Time Mode が原因で、動的クラスのできないということです。
あーなるほどねと思った方はこれからの内容を読む必要はありませんが、どういうこと?と思った方は是非ともこれからの内容を読んでいただきたいです。
では始めます。

Tailwind CSS の導入

今回は Vue に対して、Tailwind CSS を導入します。
まず Vue プロジェクト内に入り、npm install -D tailwindcss postcss autoprefixerを実行し tailwindcss を含めた必要なモジュールをインストールします。
Tailwind CSS だけでは Vue や React などでnpm run dev を実行しても使用できないので、postcss と autoprefixier を導入しています。
以下、postcss と autoprefixier の説明を簡単に行います。
postcssは CSS ファイルの処理や圧縮行ってくれるモジュールです。
PostCSS を使用すると、CSS ファイルをコンパイルしてくれより軽量な CSS を生成したり、以下のような可読性を上げる CSS を実際使用できるような CSS に変換してくれます。

/** 入力値 */
.block {
  display: block;
  &__element {
    display: block;
  }
}
/** PostCSSによる出力値 */
.block {
  display: block;
}
.block__element {
  display: block;
}

autoprefixerは PostCSS の拡張機能となっています。
この機能は CSS プロパティに対して、ベンダープレフィックスを適用してくれます。
-webkit- をはじめとするブラウザ依存に関わるプロパティを自動で設定しくれます。
さらには、導入ほやほやの CSS プロパティで動作が安定しないものを、既存のプロパティで再現しようとしてくれます。
詳細はこちらの記事を参照してください。
必要なモジュールのインストールが完了したら、npx tailwind init -pを実行し tailwind.config.js と postcss.config.js をプロジェクトに作成します。
なお、p オプションは postcss.config.js を作成するためのオプションとなっています。
これがないと postcss の機能が使えないはずなので、合わせて実行するようにします。
設定ファイルを作成したら、tailwind.config.js を以下のコードにします。

/** @type {import('tailwindcss').Config} */
export default {
  content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};

content には読み取るファイルのパスを指定しています。
そして、src 配下に生成されている CSS ファイルに以下のコードを記載します。

@tailwind base;
@tailwind components;
@tailwind utilities;

これによって、Tailwind CSS の要素を読み込むことができます。
後は App.vue の template 部分に bg-black といった Tailwind CSS の要素を記載します。

<template>
  <div class="bg-black">
    <HelloWorld msg="Vite + Vue" />
  </div>
</template>

そして、npm run dev を実行すれば、画像のようにスタイルが適用されていることを確認できます。
2023-10-07_18h58_16.png
Tailwind CSS の導入自体はこれで完了していますが、このままではクラスを書く時に補完が効かなず、ドキュメントを都度参照せず少し不便です。
その問題を解消するためにも、Tailwind CSS IntelliSenseの導入をおすすめします。
2023-10-07_10h23_39.png
これを導入すれば、クラスを書いている時の補完であったり、クラスがどのようなスタイルを適用するかをホバーすることで確認できます。
是非とも入れてみてください。
以上で、Tailwind CSS の導入が完了しました。
次に今回の主題に関わる Tailwind CSS の機能を見ていきます。

Tailwind CSS の Just In Time Mode について

Tailwind CSS はバージョン 2.1 以降Just In Time Modeという機能を導入しました。
この機能は Tailwind CSS で設定されているスタイルの定義をプロジェクトに全てビルドするのではなく、プロジェクト内で使用されているスタイルのみをビルドする機能となっています。
Just In Time Mode は、以下のメリットがあります。

  • ビルド時間の短縮
  • 環境によって使用するスタイルを分けることができるので、開発環境だけで使用したいスタイルが本番環境で影響することが無くなる。
  • スタイルのビルド量が少なくなるので、ブラウザでの動きがより快適になる。
  • t-[-110px]といった Tailwind CSS では設定されていない独自のスタイルを適用することができる。
    このように Just In Time Mode は Tailwind CSS にとって、かなり確信的な機能となっています。
    Tailwind CSS 側もそう思ったのか、バージョン 3 以降ではデフォルトで Just In Time Mode が有効となっており、先程展開したドキュメントの設定は消失しています。
    Just In Time Mode は Tailwind CSS を使う際、当たり前に存在する機能と考えた方が良さそうですが、今回この機能に苦しめられました。
    それでは、このブログの本題である Just In Time Mode が原因で、動的クラスが反映されないことについて話していきます。

Just In Time Mode がもたらす動的クラスの不都合

ここからは Just In Time Mode の注意点について解説していきます。
まず、Vue プロジェクトを作成したときに生成される components/HelloWorld.vue を以下のように変更します。

<script setup lang="ts">
withDefaults(defineProps<{ heightNum: number }>(), { heightNum: 16 });
</script>
<template>
  <div class="border-2" :class="`h-${heightNum}`">
    <p>テスト</p>
  </div>
</template>

上記コードは親側でコンポーネントを呼び出すときに、高さの数値を指定してマウントすることで、指定の高さを確保して表示することを意図して記載しております。
そして、親側で特に値を指定しなければh-16の高さを持って表示するようにしています。
これが意図通り動いたイメージ図は以下の通りです。
2023-10-07_19h10_41.png
「テスト」の文字列の下に高さが確保されており、スタイルが適用されていることを確認できます。
では、実際の動きを確認してみます。
親側で以下のように呼び出してみて、画面を確認してみます。

<template>
  <div>
    <HelloWorld :height-num="24" />
  </div>
</template>

すると、画像のようにクラスはちゃんと付与されているのに、スタイルが効いていません。
2023-10-07_19h14_58.png
24 という数値がない可能性も考えましたが、ドキュメントを確認するとちゃんと値として存在します。
これの原因ですが、Tailwind CSS の Just In Time Mode が原因となっています。
先程説明したように、Tailwind CSS は使用していないクラスについてコードを生成しないようになっています。
そして、この使う・使わないは静的な解析によって行われます。
そのため、今回のような動的にクラスを設定するような書き方では Tailwind CSS 側が使用していると認識せず、スタイルが適用されない事象が発生します。
Just In Time Mode はとても便利な機能ですが、動的クラスを使用する場合はこのような注意が必要となります。

対応方法

最後に Tailwind CSS を使いつつも、動的クラスを設定する方法について記載していきます。

①CSS In JS のモジュール内で記載する。

emotion をはじめとする CSS を JS 内で記載するライブラリなどを導入すれば、動的クラスであっても Tailwind CSS のスタイルを適用することができそうです。
ただ、Vue3 においていい感じにクラス属性をできるライブラリがなかったので、具体的にこのライブラリなら解消できますよといったことまでは言及できません。
申し訳ございません。

② 動的クラスの生成方法を変更する

動的クラスは適用できないと言っていましたが、全ての動的クラスが適用できないわけではありません。
部分的に動的とすることはできませんが、クラス全体を動的にすれば問題なくスタイルは適用されます。
具体的に見ていきます。
先程作成した HelloWorld.vue を次のように変更します。

<script setup lang="ts">
const props = withDefaults(defineProps<{ large: boolean }>(), { large: false });
const heightClass = props.large ? "h-24" : "h-16";
</script>
<template>
  <div class="border-2" :class="heightClass">
    <p>テスト</p>
  </div>
</template>

props に true が渡された時はh-24 を変数に設定し、それ以外の時はh-16 を設定します。
これをクラス属性に適用するという形になっています。
では、親側で large 属性に true を渡してページを表示させてみると、画像のようにh-24 のクラスが設定されていて、スタイルも適用できています。
2023-10-07_19h49_51.png

③CSS ファイルで使用するスタイルを読み込んでおく

Just In Time Mode では静的に使用が確認できないスタイルを読みこまないという特徴がありました。
なら、CSS ファイルのプロパティで予め読み込んでおけば Tailwind CSS が使用するスタイルだと認識できます。
具体的には以下のようなコードを CSS ファイルに記載します。

.height-16 {
  @apply h-16;
}

@apply は Tailwind CSS のスタイルを適用するという意味です。
そして、HelloWorld.vue を以下のように置き換えます。

<script setup lang="ts">
withDefaults(defineProps<{ heightNum: number }>(), { heightNum: 16 });
</script>
<template>
  <div class="border-2" :class="`height-${heightNum}`">
    <p>テスト</p>
  </div>
</template>

こうすれば props に 16 の値が渡された時、Tailwind CSS のh-16のスタイルが適用できるようになります。
ただ、注意点として CSS ファイルで読み込んでだ値しか使用できません。
今回の例だと、h-16のスタイルは使用できますがそれ以外のh-24 といった値は props で指定しても、CSS ファイルで読み込んでいないのでスタイルは適用されません。

④tailwind.config.js で必ず読み込むクラスを指定しておく

最後に tailwind.config.js で使用するクラスを指定する方法です。
方向性としては「③CSS ファイルで使用するスタイルを読み込んでおく」と一緒ですが、記載する場所が違います。
tailwind.config.js には safelist プロパティが存在しており、ここに Tailwind CSS が提供しているクラス属性で使用したいものを配列で渡すと、例えアプリ側で使用していなくても必ずスタイルは Tailwind CSS 側で読み込むようになります。
tailwind.config.js に指定する具体的な書き方は以下の通りです。

/** @type {import('tailwindcss').Config} */
export default {
  content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
  //明示的に読み込むクラスを指定する
  safelist: ["h-16"],
};

このように行えば以下のコード HelloWorld.vue でも、props に 16 が渡された場合のみ問題なくスタイルが適用されます。

<script setup lang="ts">
withDefaults(defineProps<{ heightNum: number }>(), { heightNum: 16 });
</script>
<template>
  <div class="border-2" :class="`h-${heightNum}`">
    <p>テスト</p>
  </div>
</template>

以上対応方法となっています。
最善と呼べる方法はないかもしれませんが、実装の要望に合わせて一番適したものを選択していければと思います。

おわりに

今回は Tailwind CSS の Just In Time Mode についてと、それが引き起こす問題について見ていきました。
正直理由がわからなくてもどうにか動かせてしまうので、根本の原因を見つけるのが想像以上に大変でした。
ただ、調査の過程で Just In Time Mode という Tailwind CSS の機能が知れたのは良かったと思います。
まだまだ Tailwind CSS の機能は豊富にありそうなので、引き続き調査していけたらと思います。
ここまで読んでいただきありがとうございました。

Discussion