TailwindCSS(v3.0)で動的なクラス付けができないパターンがあって困った話
開発環境
- react 18.2.0
- next 13.3.2
- tailwindcss 3.3.2
前提
NGなパターンとOKなパターンをいくつか紹介します。
本題
TailwindCSSはv3.0からデフォルトでJIT engineを搭載しています。
JIT engineはプロジェクトで使われているクラスに該当するスタイルだけを生成するようになっています。スタイルの生成方法はコードの静的解析から該当するものがあれば、そのスタイルを出力する仕組みになっています。
その結果、動的にクラス名を生成するとスタイルが出力されない?ことになります。
実際にいくつかのパターンで試してみます。
まずはベースとなるコンポーネントを実装します。
ここでは、動的なスタイルが必要になるプログレスバーのコンポーネントにします。
import type { FC } from 'react'
interface ProgressBarProps {
progressNum: number
}
export const ProgressBar: FC<ProgressBarProps> = ({ progressNum }) => (
<div className='w-full rounded-full bg-gray-200'>
<div className='w-[45%] rounded-full bg-blue-600 p-0.5 text-center text-xs font-medium leading-none text-blue-100'>
45%
</div>
</div>
)
コンポーネントのレイアウトイメージ
45%を固定にしていますが、動的に10%や78%などにしたいです。
そのためにprogressNumで外から値を設定できるようにしています。
ではNGパターンを作っていきます。
NGパターン① - リテラルで変数を埋め込み
import type { FC } from 'react'
interface ProgressBarProps {
progressNum: number
}
export const ProgressBar: FC<ProgressBarProps> = ({ progressNum }) => (
<div className='w-full rounded-full bg-gray-200'>
<div
className={`w-[${progressNum}%] rounded-full bg-blue-600 p-0.5 text-center text-xs font-medium leading-none text-blue-100`}
>
{progressNum}%
</div>
</div>
)
これができると楽ですが、NGです。
NGパターン② - オブジェクトを生成してからマッピング→リテラルで変埋め込み
import type { FC } from 'react'
interface ProgressBarProps {
progressNum: number
}
const widthDict = Object.fromEntries(Array.from({ length: 101 }, (_, i) => [i, `w-[${i}%]`]))
// const widthDict = {
// 0: 'w-[0%]',
// 1: 'w-[1%]',
// 2: 'w-[2%]',
// :
// :
// 100: 'w-[100%]',
// }
export const ProgressBar: FC<ProgressBarProps> = ({ progressNum }) => (
<div className='w-full rounded-full bg-gray-200'>
<div
className={`${widthDict[progressNum]} rounded-full bg-blue-600 p-0.5 text-center text-xs font-medium leading-none text-blue-100`}
>
{progressNum}%
</div>
</div>
)
NGパターン③ - 親子関係のないファイルからの定数インポート
import type { FC } from 'react'
import { widthDict } from '@/constants'
interface ProgressBarProps {
progressNum: number
}
export const ProgressBar: FC<ProgressBarProps> = ({ progressNum }) => (
<div className='w-full rounded-full bg-gray-200'>
<div
className={`${widthDict[progressNum]} rounded-full bg-blue-600 p-0.5 text-center text-xs font-medium leading-none text-blue-100`}
>
{progressNum}%
</div>
</div>
)
親子関係のないファイル(この例では階層の違うconstants.tsというファイルからインポートしている)から参照してもNGです。
OKなパターン① - クラス名をハードコーディングして外からpropsで渡してマッピング
import type { FC } from 'react'
interface ProgressBarProps {
progressNum: number
widthDict: Record<number, string>
}
export const ProgressBarRe: FC<ProgressBarProps> = ({ progressNum, widthDict }) => (
<div className='w-full rounded-full bg-gray-200'>
<div
className={`${widthDict[progressNum]} rounded-full bg-blue-600 p-0.5 text-center text-xs font-medium leading-none text-blue-100`}
>
{progressNum}%
</div>
</div>
)
ちなみにwidthDictはこんな感じになっています。
const widthDict = {
0: 'w-[0%]',
1: 'w-[1%]',
2: 'w-[2%]',
:
:
100: 'w-[100%]',
}
もちろん、親からwidthDictを渡さなくても、自分で定義してもスタイルはあたります。
OKなパターン② - style属性を利用する
import type { FC } from 'react'
interface ProgressBarProps {
progressNum: number
}
export const ProgressBarRe: FC<ProgressBarProps> = ({ progressNum }) => (
<div className='w-full rounded-full bg-gray-200'>
<div
className='rounded-full bg-blue-600 p-0.5 text-center text-xs font-medium leading-none text-blue-100'
style={{ width: progressNum + '%' }}
>
{progressNum}%
</div>
</div>
)
さいごに
親子関係のあるコンポーネント内でクラス名が完全な形で存在していないとクラスが割り当てられないようです。(間違っていたらご指摘ください・・)
今回の例で扱ったコンポーネントのように、0~100までのクラスを用意するのはとても冗長で避けたいですが、現状いいやり方が見つかっていないです。
さいごに紹介したOKなパターン②については使い勝手は良いですが、そもそもReact公式がstyle属性の利用を推奨していないので、悩ましいところです。