mcpField
W
web-styling-cva

web-styling-cva

github.com/agents-incデザイン・UI26140 回インストール2 日前に更新
リポジトリ

web-styling-cva について

Class Variance Authority(CVA)で型付きのビジュアルバリアントを構成するためのスキルガイドです。UI 開発者向けに、複合状態や複数パーツのコンポーネントを含め、スタイル定義と TypeScript のプロパティ型を連動させます。

主な機能

  1. 01ベースクラスとバリアントグループの定義
  2. 02compoundVariants による組み合わせスタイル
  3. 03VariantProps によるプロパティ型の導出
  4. 04省略可能なバリアントのデフォルト設定
  5. 05cx() によるスタイルの合成

活用シーン

  1. 01サイズ、意図、状態のバリアント設計
  2. 02複数バリアントの組み合わせへのスタイル適用
  3. 03プロパティ型とスタイル定義の整合
  4. 04コンポーネント各パーツのバリアント連携

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/agents-inc/skills --skill web-styling-cva

インストール後、Claude Code で/web-styling-cvaを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

metadata.yaml
reference.md
examples/
composition.md
compound-variants.md
core.md

詳細

CVA(Class Variance Authority)のパターン

クイックガイド: cva() を使うと、コンポーネントの見た目の状態を、型付けされた1つの設定オブジェクトにまとめられます。設定には、ベースクラス、バリアントグループ(サイズ、意図、状態)、独自のスタイルが必要な組み合わせに使う compoundVariants、呼び出し元が省略した場合の値を指定する defaultVariants が含まれます。VariantProps<typeof variants> を使うと、このオブジェクトからプロパティの型を導出できます。出力されるのはクラス名の文字列であり、クラス名の意味は定義しないため、ユーティリティクラス、CSS モジュール、通常の CSS のいずれとも併用できます。

詳細なリソース:

  • examples/core.md — 基本的なバリアント、ブール値の状態、複数のバリアントグループ、必須のバリアント
  • examples/compound-variants.md — 複数条件のスタイル、配列構文、状態マトリクス
  • examples/composition.md — VariantProps、スコープ付きスタイルシートのクラス名、呼び出し箇所での追加とマージ、複数パーツのコンポーネント、バリアントの拡張
  • reference.md — API シグネチャ、パターン選択表、リリース前チェックリスト

<critical_requirements>

CVA コードを書く前に

すべてのバリアントの選択肢を variants オブジェクト内に定義してください。 条件付きのクラスロジックを cva() の外に置くと、定義が二分され、導出された型がコンポーネントを正確に表せなくなります。

VariantProps<typeof variants> でプロパティの型を抽出してください。 手書きのバリアント型は、バリアントを追加したり名前を変更したりした時点で定義とずれてしまいます。導出された型ならその心配はありません。

呼び出し元が省略する可能性のあるすべてのバリアントに defaultVariants を設定してください。 そうしないと、引数なしで呼び出した場合、ベースクラスだけが返され、意図やサイズは反映されません。

複数のバリアントの組み合わせに独自のスタイルが必要な場合は、compoundVariants を使ってください。 呼び出し箇所ごとに記述するのではなく、定義内に「X と Y の場合は、さらに Z を適用する」と一度だけ記述できます。

</critical_requirements>


自動検出: cva、class-variance-authority、VariantProps、compoundVariants、defaultVariants、cx

対象:

  • サイズ、意図、トーン、状態など、見た目の軸が複数あるコンポーネント
  • 独自のスタイルが必要な組み合わせ(「大きいプライマリーは大文字」など)
  • スタイル定義からコンポーネントのプロパティ型を導出する場合
  • パーツ間の見た目を連動させる必要がある、複数パーツのコンポーネント

ここでは扱わない内容:

  • クラス名の意味 — cva() が出力するのは文字列です。その背後にある CSS は、それを管理する仕組みによって決まります
  • 競合する2つのクラス名の競合解決 — cx() は値を連結し、偽と判定される値を除外します。px-2 と px-8 のどちらを優先するかは、マージユーティリティの役割です
  • ビューポートに応じて変化するスタイル — バリアントはレンダリングごとに一度選択されるため、ブレークポイントはスタイルシートで扱います
  • 実行時に計算される値(計測した幅や、データに応じた色など) — バリアントは選択肢が固定されているため、実行時の値はインラインスタイルやカスタムプロパティで扱います

CVA はバリアントを、UI の状態を表す型システムとして扱います。バリアントの一覧が唯一の信頼できる情報源です。クラス文字列とプロパティ型の両方がそこから生成されるため、選択肢を追加する箇所は1つで済み、残りは TypeScript が反映します。定義が表すのは各状態が どのように見えるか であり、クラスリストの計算方法ではありません。そのため、同じ定義を変更せずに、さまざまな UI フレームワークや CSS の手法に適用できます。


基本パターン

パターン 1:基本的なバリアント定義

ベースクラスに、各バリアントグループの項目を加えます。グループ内のクラスが2つを超える場合は、スペース区切りの文字列より配列のほうが読みやすくなります。

import { cva } from "class-variance-authority";

const buttonVariants = cva(["font-semibold", "border", "rounded"], {
  variants: {
    intent: {
      primary: ["bg-blue-600", "text-white"],
      secondary: ["bg-white", "text-gray-800"],
    },
    size: {
      sm: ["text-sm", "py-1", "px-2"],
      md: ["text-base", "py-2", "px-4"],
    },
  },
  defaultVariants: { intent: "primary", size: "md" },
});

buttonVariants(); // primary + md
buttonVariants({ intent: "secondary" }); // secondary + md

全コード:examples/core.md


パターン 2:ブール値のバリアント

true/false のキーで二値の状態を表します。両方の状態を定義してください。通常状態のスタイルは false 側に記述します。

const inputVariants = cva(["border", "rounded", "px-3", "py-2"], {
  variants: {
    disabled: {
      false: ["bg-white", "cursor-text"],
      true: ["bg-gray-100", "cursor-not-allowed"],
    },
    error: { false: ["border-gray-300"], true: ["border-red-500"] },
  },
  defaultVariants: { disabled: false, error: false },
});

全コード:examples/core.md


パターン 3:複合バリアント

compoundVariants のエントリーは、そのすべてのキーが一致すると適用されます。配列の値はその要素のいずれかに一致するため、ほぼ同一のルールをまとめられます。

const buttonVariants = cva(["font-semibold", "rounded"], {
  variants: {
    intent: { primary: ["bg-blue-600"], secondary: ["bg-white"] },
    disabled: { false: null, true: ["opacity-50", "cursor-not-allowed"] },
  },
  compoundVariants: [
    // Hover only where the button is interactive
    { intent: "primary", disabled: false, class: ["hover:bg-blue-700"] },
    { intent: "secondary", disabled: false, class: ["hover:bg-gray-100"] },
    {
      intent: ["primary", "secondary"],
      disabled: true,
      class: ["pointer-events-none"],
    },
  ],
  defaultVariants: { intent: "primary", disabled: false },
});

コード全文:examples/compound-variants.md


パターン 4:VariantProps による型の抽出

VariantProps は定義を読み取り、バリアントグループごとに、オプションかつ nullable なプロパティを生成します。

import { cva, type VariantProps } from "class-variance-authority";

const cardVariants = cva(["rounded-lg", "border"], {
  variants: {
    elevation: { flat: ["shadow-none"], raised: ["shadow-md"] },
    padding: { none: ["p-0"], sm: ["p-2"], md: ["p-4"] },
  },
  defaultVariants: { elevation: "flat", padding: "md" },
});

type CardVariants = VariantProps<typeof cardVariants>;
// { elevation?: "flat" | "raised" | null; padding?: "none" | "sm" | "md" | null }

// Make one variant mandatory: leave it out of defaultVariants, then narrow the type
type CardProps = Omit<CardVariants, "padding"> &
  Required<Pick<CardVariants, "padding">>;

コード全文:examples/composition.md


パターン 5:cx() によるクラスのマージ

cx() はパッケージに同梱されています(clsx の再エクスポートです)。値を連結し、falsy な値を取り除きます。競合の解決は別のユーティリティの役割です。

import { cva, cx } from "class-variance-authority";

cx(buttonVariants({ intent: "primary" }), highlighted && "ring-2", className);

// Where a caller's className must be able to override a variant's own padding,
// wrap the call in a merging utility instead of concatenating
function button(variants: ButtonVariants, className?: string): string {
  return cn(buttonVariants(variants), className);
}

コード全文:examples/composition.md


パターン 6:複数パーツから成るコンポーネント

スタイルを適用するパーツごとに cva() を用意し、バリアント名を共有することで、パーツを連動させます。

const formFieldVariants = {
  label: cva(["block", "font-medium"], {
    variants: { size: { sm: ["text-sm"], md: ["text-base"] } },
    defaultVariants: { size: "md" },
  }),
  input: cva(["w-full", "border", "rounded"], {
    variants: {
      size: { sm: ["text-sm", "px-2"], md: ["text-base", "px-3"] },
      error: { false: ["border-gray-300"], true: ["border-red-500"] },
    },
    defaultVariants: { size: "md", error: false },
  }),
  helper: cva(["mt-1"], {
    variants: {
      size: { sm: ["text-xs"], md: ["text-sm"] },
      error: { false: ["text-gray-500"], true: ["text-red-600"] },
    },
    defaultVariants: { size: "md", error: false },
  }),
};

コード全文:examples/composition.md


パターン 7:定義の合成と拡張

2 つの定義を cx() で組み合わせます。共有するインタラクティブなベースと、コンポーネント独自のバリアントを合わせます。それぞれの VariantProps を交差させて、1 つのプロパティ型にします。

const interactiveVariants = cva(["transition-colors", "focus:ring-2"], {
  variants: { focusRing: { blue: ["focus:ring-blue-500"] } },
  defaultVariants: { focusRing: "blue" },
});

type ButtonProps = VariantProps<typeof interactiveVariants> &
  VariantProps<typeof buttonVariants>;

function button({ focusRing, intent }: ButtonProps): string {
  return cx(interactiveVariants({ focusRing }), buttonVariants({ intent }));
}

コード全文:examples/composition.md


<red_flags>

注意すべき点

意図しないクラスが生成される場合:

  • defaultVariants がない — buttonVariants() はベースクラスしか返さないため、プロパティなしでレンダリングされたコンポーネントでは、意図した見た目やサイズが失われます。すべてのオプションのバリアントにデフォルト値を設定してください。
  • ブール値のバリアントに true キーしかない — 通常の状態にはクラスが一切適用されません。false 分岐にベースのスタイルを記述するか、実際にクラスが不要な場合は null を指定してください。
  • バリアントのプロパティ型を手書きする — 定義から削除された値を受け入れたり、追加された値を拒否したりします。VariantProps<typeof variants> なら定義とのずれを防げます。
  • 定義の外で条件付きクラスを処理する — バリアントテーブルが唯一の信頼できる情報源ではなくなり、導出された型もコンポーネントの半分しか表さなくなります。条件をバリアントとして追加してください。
  • 組み合わせた状態にネストされた三項演算子を使う — 到達不能な分岐が見えなくなります。代わりに compoundVariants で組み合わせを列挙してください。

予想外の動作:

  • VariantProps はすべてのバリアントをオプションかつ nullable にします。必須にするには Required<Pick<T, "color">> を使います。
  • compoundVariants のクラスは、返される文字列内で通常のバリアントクラスの後に配置されます。文字列の順序は CSS の優先順位を意味しないため、競合するクラス名には引き続きマージ用ユーティリティが必要です。
  • ベースクラスは常に適用され、どのバリアントもそれを削除できません。一部の状態でのみ必要なクラスは、ベースではなくバリアントに記述してください。
  • null は有効なバリアント値で、「このオプションは何も追加しない」という意味です。キーを省略してオプションを未定義のままにする場合とは異なります。
  • compoundVariants のエントリーでは class と className のどちらも使えます。プロジェクト内ではどちらか一方を選んでください。混在させると、エントリーの形が異なるように見えてしまいます。

</red_flags>

コメント