cva の使い方

Awesome node module

v0.0.0/週MITユーティリティ
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

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と組み合わせることで、コンポーネントのスタイル管理を劇的に整理できます。

どんな時に使う?

  1. Buttonコンポーネントに size="lg"variant="destructive" のようなProps駆動のスタイル切り替えを実装したい時 — バリアントごとのクラス名を条件分岐で書く煩雑さから解放されます
  2. デザインシステムやUIライブラリを構築する時 — 各コンポーネントのバリアント定義を一元管理し、TypeScriptの型推論で安全に利用できます
  3. 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-authoritytailwind-variantsclsx / classnames
バリアント定義✅ 中核機能✅ 中核機能❌ なし
TypeScript型推論VariantProps✅ 組み込み❌ なし
複合バリアントcompoundVariantscompoundVariants❌ なし
スロット(複数要素)❌ なしslots 対応❌ なし
レスポンシブバリアント❌ なし✅ 対応❌ なし
クラス競合の解決❌ 別途 twMerge 必要✅ 内蔵 (twMerge)❌ なし
バンドルサイズ小さい(~1.5KB)やや大きい最小(~0.5KB)
学習コスト低い中程度最低

選定の目安:

  • シンプルにバリアント管理したいclass-variance-authority
  • スロットやレスポンシブバリアントも欲しいtailwind-variants
  • 条件付きクラス結合だけで十分clsx

注意点・Tips

1. tailwind-merge との併用が実質必須

CVA単体ではTailwindのクラス競合(例: p-6p-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 ヘルパーパターンを押さえておけば、実