cva(npm: cva@0.0.0)の使い方解説
⚠️ 重要な注意事項: npm上の
cva@0.0.0はプレースホルダーパッケージであり、実際の機能を持ちません。この記事では、多くの開発者が探しているであろう Class Variance Authority(class-variance-authority) について解説します。CVAとして広く知られているのはこちらのパッケージです。
一言でいうと
Class Variance Authority(CVA) は、UIコンポーネントのバリアント(見た目のバリエーション)をタイプセーフに定義・管理するためのユーティリティライブラリです。Tailwind CSSなどのユーティリティファーストCSSと組み合わせることで、コンポーネントのスタイル管理を劇的に整理できます。
どんな時に使う?
- Buttonコンポーネントに
size="lg"やvariant="destructive"のようなProps駆動のスタイル切り替えを実装したい時 — バリアントごとのクラス名を条件分岐で書く煩雑さから解放されます - デザインシステムやUIライブラリを構築する時 — 各コンポーネントのバリアント定義を一元管理し、TypeScriptの型推論で安全に利用できます
- Tailwind CSSのクラス名が肥大化して可読性が落ちている時 — バリアントごとにクラスを分離して整理できます
インストール
実際に使うパッケージは class-variance-authority です。
# npm
npm install class-variance-authority
# yarn
yarn add class-variance-authority
# pnpm
pnpm add class-variance-authority
※
npm install cvaではありません。cvaはプレースホルダーパッケージです。
基本的な使い方
最も典型的なパターンとして、Buttonコンポーネントのバリアントを定義する例を示します。
// button.ts
import { cva, type VariantProps } from "class-variance-authority";
const button = cva(
// ベースクラス(全バリアント共通)
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none",
{
variants: {
variant: {
primary: "bg-blue-600 text-white hover:bg-blue-700",
secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",
destructive: "bg-red-600 text-white hover:bg-red-700",
ghost: "hover:bg-gray-100 text-gray-700",
},
size: {
sm: "h-8 px-3 text-sm",
md: "h-10 px-4 text-base",
lg: "h-12 px-6 text-lg",
},
},
defaultVariants: {
variant: "primary",
size: "md",
},
}
);
// 使用例
button();
// => "inline-flex items-center ... bg-blue-600 text-white hover:bg-blue-700 h-10 px-4 text-base"
button({ variant: "destructive", size: "lg" });
// => "inline-flex items-center ... bg-red-600 text-white hover:bg-red-700 h-12 px-6 text-lg"
Reactコンポーネントでの使用例
// Button.tsx
import React from "react";
import { cva, type VariantProps } from "class-variance-authority";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md font-medium transition-colors",
{
variants: {
variant: {
primary: "bg-blue-600 text-white hover:bg-blue-700",
secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",
destructive: "bg-red-600 text-white hover:bg-red-700",
},
size: {
sm: "h-8 px-3 text-sm",
md: "h-10 px-4 text-base",
lg: "h-12 px-6 text-lg",
},
},
defaultVariants: {
variant: "primary",
size: "md",
},
}
);
// VariantPropsで型を自動抽出
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
VariantProps<typeof buttonVariants>;
export const Button: React.FC<ButtonProps> = ({
variant,
size,
className,
...props
}) => {
return (
<button
className={buttonVariants({ variant, size, className })}
{...props}
/>
);
};
// 使用側:TypeScriptが variant と size の値を補完してくれる
<Button variant="destructive" size="lg">削除</Button>
よく使うAPI
1. cva() — バリアント定義の作成
CVAの中核関数です。ベースクラスとバリアント設定を受け取り、クラス名を生成する関数を返します。
import { cva } from "class-variance-authority";
const badge = cva("rounded-full font-semibold", {
variants: {
color: {
info: "bg-blue-100 text-blue-800",
success: "bg-green-100 text-green-800",
warning: "bg-yellow-100 text-yellow-800",
},
size: {
sm: "px-2 py-0.5 text-xs",
md: "px-3 py-1 text-sm",
},
},
defaultVariants: {
color: "info",
size: "sm",
},
});
badge({ color: "success", size: "md" });
// => "rounded-full font-semibold bg-green-100 text-green-800 px-3 py-1 text-sm"
2. VariantProps<T> — バリアントの型抽出
定義したバリアントからTypeScriptの型を自動生成します。コンポーネントのProps定義に不可欠です。
import { cva, type VariantProps } from "class-variance-authority";
const alertVariants = cva("p-4 rounded-lg border", {
variants: {
severity: {
info: "bg-blue-50 border-blue-200",
error: "bg-red-50 border-red-200",
},
},
defaultVariants: {
severity: "info",
},
});
type AlertVariants = VariantProps<typeof alertVariants>;
// => { severity?: "info" | "error" | null | undefined }
3. compoundVariants — 複合バリアント
複数のバリアントの組み合わせに対して追加のクラスを適用できます。
const button = cva("rounded font-medium", {
variants: {
intent: {
primary: "bg-blue-600 text-white",
secondary: "bg-gray-200 text-gray-900",
},
size: {
sm: "text-sm px-3 py-1",
lg: "text-lg px-6 py-3",
},
},
compoundVariants: [
{
// primary + lg の組み合わせの時だけ追加されるクラス
intent: "primary",
size: "lg",
class: "uppercase tracking-wide",
},
{
// secondary + sm の組み合わせ
intent: "secondary",
size: "sm",
class: "border border-gray-300",
},
],
defaultVariants: {
intent: "primary",
size: "sm",
},
});
button({ intent: "primary", size: "lg" });
// => "rounded font-medium bg-blue-600 text-white text-lg px-6 py-3 uppercase tracking-wide"
4. className の追加渡し
生成関数の引数に className を渡すことで、外部から追加のクラスを注入できます。
const card = cva("rounded-lg shadow-md p-6", {
variants: {
padding: {
compact: "p-3",
normal: "p-6",
spacious: "p-10",
},
},
defaultVariants: {
padding: "normal",
},
});
// 外部からクラスを追加
card({ padding: "compact", className: "mt-4 border border-gray-200" });
// => "rounded-lg shadow-md p-3 mt-4 border border-gray-200"
Tips: Tailwind CSSのクラス競合を解決するには、
tailwind-mergeと組み合わせるのが定番です(後述)。
5. ベースクラスのみ(バリアントなし)の使い方
バリアントが不要な場合でも、再利用可能なクラス定義として使えます。
const container = cva("mx-auto max-w-7xl px-4 sm:px-6 lg:px-8");
container();
// => "mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"
container({ className: "py-12" });
// => "mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-12"
類似パッケージとの比較
| 特徴 | class-variance-authority | tailwind-variants | clsx / classnames |
|---|---|---|---|
| バリアント定義 | ✅ 中核機能 | ✅ 中核機能 | ❌ なし |
| TypeScript型推論 | ✅ VariantProps | ✅ 組み込み | ❌ なし |
| 複合バリアント | ✅ compoundVariants | ✅ compoundVariants | ❌ なし |
| スロット(複数要素) | ❌ なし | ✅ slots 対応 | ❌ なし |
| レスポンシブバリアント | ❌ なし | ✅ 対応 | ❌ なし |
| クラス競合の解決 | ❌ 別途 twMerge 必要 | ✅ 内蔵 (twMerge) | ❌ なし |
| バンドルサイズ | 小さい(~1.5KB) | やや大きい | 最小(~0.5KB) |
| 学習コスト | 低い | 中程度 | 最低 |
選定の目安:
- シンプルにバリアント管理したい →
class-variance-authority - スロットやレスポンシブバリアントも欲しい →
tailwind-variants - 条件付きクラス結合だけで十分 →
clsx
注意点・Tips
1. tailwind-merge との併用が実質必須
CVA単体ではTailwindのクラス競合(例: p-6 と p-3 が両方残る)を解決しません。cn ヘルパーを作るのが定番パターンです。
// lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// コンポーネント側
<button className={cn(buttonVariants({ variant, size }), className)} />
2. Tailwind CSS IntelliSenseの設定
VSCodeでバリアント内のクラスにも補完を効かせるには、.vscode/settings.json に以下を追加します。
{
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
]
}
3. null でバリアントを無効化できる
defaultVariants を設定しつつ、特定の場面でそのバリアントを適用したくない場合は null を渡します。
const text = cva("font-sans", {
variants: {
color: {
primary: "text-blue-600",
muted: "text-gray-500",
},
},
defaultVariants: {
color: "primary",
},
});
text({ color: null });
// => "font-sans"(colorバリアントが適用されない)
4. ファイル分割のベストプラクティス
バリアント定義はコンポーネントファイルと同じファイルに置くか、*.variants.ts として分離するのが一般的です。shadcn/uiでは同一ファイル内に定義するパターンが採用されています。
components/
ui/
button.tsx # buttonVariants + Buttonコンポーネント
badge.tsx # badgeVariants + Badgeコンポーネント
5. CSS-in-JSではなくクラス名の文字列操作
CVAはランタイムでCSSを生成するわけではなく、あくまでクラス名の文字列を組み立てるだけです。そのため非常に軽量で、SSRとの相性も問題ありません。
まとめ
Class Variance Authority(CVA) は、UIコンポーネントのバリアントをタイプセーフかつ宣言的に管理するためのライブラリです。Tailwind CSSとの組み合わせが主な用途で、shadcn/uiをはじめとする多くのモダンUIライブラリで採用されています。tailwind-merge + clsx と組み合わせた cn ヘルパーパターンを押さえておけば、実