Tailwind Variants(tv)の使い方|slots・compoundVariants・レスポンシブとTailwind CSS v4対応
Tailwind Variantsは、Tailwind CSSのユーティリティクラスをコンポーネント単位のバリアントAPIとしてまとめる小さなライブラリで、tv()関数で色・サイズ・状態などのスタイル切り替えを型安全に定義できます。最新はv3.2.2で、Tailwind CSS v4に対応します。なお、検索で混同されやすい@custom-variant/@variantはTailwind CSS本体(v4)のディレクティブで、このライブラリとは別物です。本記事はこの違いを含め、導入から実務で使うキーまでを一度ずつ整理します。
まとめ:Tailwind Variantsの要点(先に結論)
- 使いどころ:クラス文字列が条件分岐で肥大化する再利用コンポーネント(Button、Card等)。単発の要素には不要。
- 中核API:
tv()のbase/variants/defaultVariants/slots/compoundVariants/compoundSlots/extend。 - バージョン対応:Tailwind CSS v4 → tailwind-variants v3系+tailwind-merge v3系。Tailwind CSS v3 → tailwind-variants v0系+tailwind-merge v2.6.0。
- 混同注意:ライブラリの
tv()と、Tailwind CSS v4のネイティブ@custom-variant/@variantは無関係。用途が違う。 - 上書き:呼び出し時に
class/classNameを渡すと、内部のtailwind-mergeが競合クラスを解決して上書きする。
Tailwind Variantsの役割(tvが解決する課題)
Tailwindでコンポーネントを作ると、条件によってクラス文字列を組み立てる分岐がJSX側に散らばり、可読性と再利用性が落ちます。Tailwind Variantsは、この「クラスの出し分け」を宣言的な定義に外出しするライブラリです。設計はclass-variance-authority(cva)に近い発想ですが、複数要素をまとめるslots、レスポンシブ対応、tailwind-mergeによる競合解決を標準で備える点が異なります。特定フレームワークに依存せず、React・Vue・Solidなどで同じ書き方が使えます。
一方で、バリアントが1〜2種類しかない小規模な要素にまで導入すると、定義ファイルが増えるだけで利点が出ません。「同じコンポーネントを色・サイズ・状態違いで何度も使い回す」場面に絞って導入するのが実務的な判断基準です。
セットアップとバージョン対応(v3.2.2 / tailwind-merge / Tailwind CSS v4・v3)
インストールはパッケージを追加するだけです。tailwind-mergeはv2以降任意のpeer dependencyで、className上書きの競合解決を使う場合に別途入れます。v3.2.2のpeer指定はtailwind-merge >= 3.0.0のため、Tailwind CSS v4環境では次の組み合わせになります。
| Tailwind CSS | tailwind-variants | tailwind-merge |
|---|---|---|
| v4 | v3系(最新 v3.2.2) | v3系(v3.0.0以上) |
| v3 | v0系 | v2.6.0 |
# Tailwind CSS v4 環境
npm install tailwind-variants tailwind-merge
# 読み込み
import { tv } from 'tailwind-variants'
なお単純な文字列連結(競合解決なし)を行うヘルパーは、v3.2.0でcnBaseがcxに置き換えられました。旧名を使っている場合はcxへ移行します。変動が速い箇所なので、対応バージョンの最終確認は公式のアップグレードガイドで行ってください。
tv()の基本構成(base・variants・defaultVariants)
tv()には共通クラスのbase、切り替え定義のvariants、既定値のdefaultVariantsを渡します。戻り値の関数に選択したバリアントを渡すと、最終的なクラス文字列が返ります。
import { tv } from 'tailwind-variants'
const button = tv({
base: 'rounded font-medium text-white',
variants: {
color: { primary: 'bg-blue-500', secondary: 'bg-gray-500' },
size: { sm: 'text-sm p-2', md: 'text-base p-4' },
},
defaultVariants: { color: 'primary', size: 'md' },
})
button() // primary + md(既定)
button({ color: 'secondary', size: 'sm' })
評価語ではなくキー名で意図が読めるため、JSX側はclassName={button({ color })}だけになり、分岐がJSXから消えます。
slotsによる複数要素のまとめ定義
カードやモーダルのように親要素と子要素をまとめて扱う場合はslotsを使います。スロット数に上限はなく、variants内でも各スロット別にクラスを指定できます。呼び出すと各スロットの関数を持つオブジェクトが返ります。
const card = tv({
slots: { base: 'rounded-lg border', title: 'font-bold', body: 'text-sm' },
variants: {
tone: { info: { base: 'border-blue-300', title: 'text-blue-700' } },
},
})
const { base, title, body } = card({ tone: 'info' })
// base(), title(), body() をそれぞれ className へ
compoundVariants・compoundSlotsによる交差条件スタイル
「色がsecondaryかつサイズがsmのときだけ枠線を足す」のように複数バリアントの組み合わせに依存するスタイルはcompoundVariantsで定義します。条件に一致したときだけclassが追加されます。slots構成で「特定条件×特定スロット」を狙う場合はcompoundSlotsを使います。
const button = tv({
base: 'rounded text-white',
variants: {
color: { primary: 'bg-blue-500', secondary: 'bg-gray-500' },
size: { sm: 'p-2', md: 'p-4' },
},
compoundVariants: [
{ color: 'secondary', size: 'sm', class: 'border border-gray-400' },
],
})
個々のvariantに条件分岐を書き足すと組み合わせ爆発を起こしますが、compoundVariantsに「交差条件」を集約すると定義が一箇所にまとまります。slots構成で「特定条件のときに特定スロットだけ変える」場合はcompoundSlotsを使い、対象スロットをslots配列で指定します。
const modal = tv({
slots: { base: 'rounded', close: 'text-gray-400' },
variants: { size: { sm: {}, lg: {} } },
compoundSlots: [
{ slots: ['close'], size: 'lg', class: 'text-gray-600' },
],
})
レスポンシブVariantsの設定(responsiveVariants)
ブレークポイントごとにバリアントを切り替えるには、tv()の第2引数でresponsiveVariantsに対象ブレークポイントを渡します。これを有効にすると、呼び出し時にバリアント値をブレークポイントのオブジェクトで指定できます。
const button = tv(
{
base: 'text-white',
variants: { size: { sm: 'text-sm p-2', md: 'text-base p-4', lg: 'text-lg p-6' } },
},
{ responsiveVariants: ['md', 'lg'] }
)
button({ size: { initial: 'sm', md: 'md', lg: 'lg' } })
生成されるレスポンシブ用クラスがビルド時に削除されないよう、対象ブレークポイントは事前に列挙しておく必要があります。JSコンフィグを使う構成では、tailwind-variants/transformerのwithTVでtailwind.config.jsをラップして検出対象に含めます。設定方式はTailwind CSSのバージョンで異なるため、最新手順は公式ドキュメントで確認してください。
classNameによる上書きとtailwind-mergeの役割
呼び出し時にclass/classNameを渡すと、定義済みクラスへ追記されます。このときp-4とp-2のような競合を後勝ちで解決するのがtailwind-mergeです。tailwind-mergeを入れていないと競合クラスが両方残り、意図しないスタイルになることがあります。
button({ color: 'primary', class: 'p-6' })
// base/variants の padding を p-6 が上書き(tailwind-merge が競合解決)
既存定義を土台に別コンポーネントを派生させたい場合はextendで継承し、差分だけを追記します。共通のデザイントークンを1つの定義に集約し、各コンポーネントがそれを拡張する構成にすると重複が減ります。
tailwind-variants(tv)とTailwind CSS v4の@custom-variant/@variantの違い
検索では@custom-variantや@variantでこの記事にたどり着く人がいますが、これらはTailwind CSS本体(v4)のディレクティブで、tailwind-variantsライブラリとは無関係です。名前が似ているため混同されがちなので、役割を分けて押さえてください。
| 機能 | 正体 | 書く場所 | 用途 |
|---|---|---|---|
| tailwind-variants(tv) | npmライブラリ | JS/TS | コンポーネントのバリアントAPIを型安全に定義 |
| @custom-variant | Tailwind CSS v4のディレクティブ | CSS | 独自バリアントを定義(v3のaddVariant後継) |
| @variant | Tailwind CSS v4のディレクティブ | CSS | 手書きCSSの中で既存バリアントを適用 |
@custom-variantは、任意のセレクタから新しいバリアント(例:data-*属性やテーマ用クラス)をCSS側で作るためのものです。v4以前にプラグインのaddVariant()で行っていた定義を置き換えます。
/* CSS 側で独自バリアントを定義 */
@custom-variant theme-midnight (&:where([data-theme="midnight"] *));
@variantは、手書きCSSのルール内で既存バリアント(darkなど)を適用するためのディレクティブです。v4のalpha期に一時@variantという名前で定義用途を担っていた経緯があり、現在は定義は@custom-variant、適用は@variantと役割が分かれています。要するに、JS/TSでコンポーネントのスタイルを出し分けたいならtailwind-variants、CSSで独自の状態バリアントを増やしたいなら@custom-variantという使い分けです。v4全体の変更点はTailwind CSS v4の主な変更点と新機能の概要で確認できます。
よくある質問
基本のバリアント定義は似ていますが、tailwind-variantsは複数要素をまとめるslots、responsiveVariantsによるレスポンシブ対応、tailwind-mergeでの競合解決を標準で備えます。cvaはより最小構成です。
Tailwind CSS v4で使えますか?
使えます。v4環境ではtailwind-variants v3系(最新v3.2.2)とtailwind-merge v3系を組み合わせます。Tailwind CSS v3のプロジェクトはtailwind-variants v0系とtailwind-merge v2.6.0が対応版です。
@custom-variantとtailwind-variantsは同じものですか?
別物です。@custom-variant/@variantはTailwind CSS v4本体のCSSディレクティブ、tailwind-variantsはtv()を提供するnpmライブラリです。用途も書く場所(CSS/JS)も異なります。
tailwind-mergeは必須ですか?
任意のpeer dependencyです。ただしclass/classNameでの上書きで競合クラスを後勝ちにしたい場合は必要です。入れないと競合クラスが両方残ります。
responsiveVariantsが効かないときは?
第2引数のresponsiveVariantsに対象ブレークポイントを列挙しているか、生成されるレスポンシブ用クラスがビルド時に削除されていないかを確認します。JSコンフィグ構成ではwithTVで設定をラップします。