Skills web-styling-cva
Class Variance Authority - type-safe component variant styling with cva(), compound variants, and VariantProps
git clone https://github.com/agents-inc/skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/agents-inc/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/dist/plugins/web-styling-cva/skills/web-styling-cva" ~/.claude/skills/agents-inc-skills-web-styling-cva && rm -rf "$T"
dist/plugins/web-styling-cva/skills/web-styling-cva/SKILL.mdCVA (Class Variance Authority) Patterns
Quick Guide: Use CVA to define type-safe component variants with a declarative API. Define base classes, variant groups (size, intent, state), compound variants for combined conditions, and default values. Extract types with
. Works with any CSS approach (utility classes, CSS modules, plain CSS). Always setVariantProps, always define bothdefaultVariants/truefor boolean variants, always usefalsefor type extraction.VariantProps
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
, named constants)import type
(You MUST define all variant options in the
object - NEVER use conditional class logic outside cva)variants
(You MUST use
to extract types - NEVER manually define variant prop types)VariantProps
(You MUST use
for initial state - NEVER rely on undefined props for defaults)defaultVariants
(You MUST use
for multi-condition styles - NEVER nest ternaries for combined states)compoundVariants
</critical_requirements>
Auto-detection: cva, class-variance-authority, VariantProps, variants, compoundVariants, defaultVariants, component variants, type-safe styling, cx
When to use:
- Building components with multiple visual variants (size, intent, state)
- Creating design system components with type-safe props
- Implementing compound conditions (e.g., "large primary" has special styles)
- Sharing variant styling across projects or frameworks
When NOT to use:
- Simple components with no variants (just use plain classes)
- One-off styling without pattern reuse
- Dynamic styles based on runtime values (use inline styles or CSS variables)
- Responsive styles that change based on viewport (use CSS media queries)
Key patterns covered:
- Basic variant definitions with
cva() - Boolean variants for toggle states
- Compound variants for combined conditions
- Type extraction with
VariantProps - Class merging with
and external utilitiescx() - Multi-part component patterns
- Composing and extending variant definitions
Detailed Resources:
- examples/core.md - Basic variants, boolean states, default values, required variants
- examples/compound-variants.md - Multi-condition styles, array syntax, state combinations
- examples/composition.md - VariantProps, class merging, multi-part components, extending variants
- reference.md - Decision frameworks, anti-patterns, quick reference
<philosophy>
Philosophy
CVA treats component variants as a type system for UI states. Instead of scattering conditional class logic throughout components, CVA centralizes variant definitions in a single, typed configuration object.
Core Principles:
- Declarative over imperative: Define what each variant looks like, not how to compute classes
- Type-safe variants: TypeScript catches invalid variant values at compile time
- Framework-agnostic: Works with any UI framework and any CSS approach
- Composition-friendly: Variants can be combined, extended, and composed
- Single source of truth: Variant definitions live in one place, types are derived
Why CVA over manual conditional classes:
</philosophy>// BAD: scattered logic, no type safety, hard to maintain function getButtonClasses(size: string, variant: string, disabled: boolean) { let classes = "btn"; if (size === "sm") classes += " btn-sm"; else if (size === "lg") classes += " btn-lg"; if (variant === "primary") classes += " btn-primary"; if (disabled) classes += " btn-disabled"; return classes; } // GOOD: declarative, type-safe, composable const buttonVariants = cva("btn", { variants: { size: { sm: "btn-sm", lg: "btn-lg" }, variant: { primary: "btn-primary" }, disabled: { true: "btn-disabled" }, }, });
<patterns>
Core Patterns
Pattern 1: Basic Variant Definition
Define component styles with base classes and variant options. Use array syntax for readability, always set
defaultVariants.
import { cva } from "class-variance-authority"; const buttonVariants = cva( ["font-semibold", "border", "rounded"], // base classes as array { 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(); // defaults: primary + md buttonVariants({ intent: "secondary" }); // secondary + md
Key rules: always provide
defaultVariants (calling without props returns incomplete classes otherwise), use arrays over space-separated strings for readability.
See examples/core.md for complete button, badge, and icon button examples.
Pattern 2: Boolean Variants
Use
true/false keys for binary states. Always define both sides.
const inputVariants = cva(["border", "rounded", "px-3", "py-2"], { variants: { disabled: { false: ["bg-white", "cursor-text"], // normal state true: ["bg-gray-100", "cursor-not-allowed"], // disabled state }, error: { false: ["border-gray-300"], true: ["border-red-500"], }, }, defaultVariants: { disabled: false, error: false }, });
Key rule: missing
false case means no styles applied in normal state -- variant logic becomes incomplete.
See examples/core.md for boolean variant patterns with loading, disabled, and error states.
Pattern 3: Compound Variants
Use
compoundVariants when specific variant combinations need special styles. Array syntax matches multiple values.
const buttonVariants = cva(["font-semibold", "rounded"], { variants: { intent: { primary: ["bg-blue-600", "text-white"], secondary: ["bg-white", "text-gray-800"], }, disabled: { false: null, true: ["opacity-50", "cursor-not-allowed"], }, }, compoundVariants: [ // Hover only when enabled { intent: "primary", disabled: false, class: ["hover:bg-blue-700"] }, { intent: "secondary", disabled: false, class: ["hover:bg-gray-100"] }, // Array syntax: matches multiple values { intent: ["primary", "secondary"], disabled: true, class: ["pointer-events-none"], }, ], defaultVariants: { intent: "primary", disabled: false }, });
Key rules: compound variants express "when X AND Y, also apply Z", array syntax avoids duplicating rules across similar variants.
See examples/compound-variants.md for hover states, loading overrides, state matrices, and multi-part compounds.
Pattern 4: Type Extraction with VariantProps
Always use
VariantProps to extract types from cva definitions -- never manually define variant types.
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" }, }); // Extract types -- always in sync with cva definition type CardVariants = VariantProps<typeof cardVariants>; // { elevation?: "flat" | "raised" | null; padding?: "none" | "sm" | "md" | null } interface CardProps extends CardVariants { children: unknown; className?: string; }
Key rule: manual types drift when you add/remove variants.
VariantProps is always in sync.
To make specific variants required (no default):
type BadgeProps = Omit<BadgeVariants, "color"> & Required<Pick<BadgeVariants, "color">>;
See examples/composition.md for complete type extraction and required variant patterns.
Pattern 5: Class Merging with cx()
Use
cx() (built-in, alias for clsx) for class concatenation. Use an external merge utility for class conflict resolution.
import { cva, cx } from "class-variance-authority"; // cx() concatenates and filters falsy values cx(buttonVariants({ intent: "primary" }), highlighted && "ring-2", className); // For conflict resolution (e.g., caller overriding variant padding), // use a class-merging utility wrapper function button(variants: ButtonVariants, className?: string): string { return cn(buttonVariants(variants), className); // cn() resolves conflicts }
See examples/composition.md for class merging patterns and conflict resolution.
Pattern 6: Multi-Part Components
Define separate cva for each styled part of a component (label, input, helper text). Share variant values for visual consistency.
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 }, }), };
See examples/composition.md for multi-part and extending/composing variant patterns.
Pattern 7: Composing and Extending Variants
Combine multiple cva definitions with
cx() for shared base + specialized variants.
const interactiveVariants = cva(["transition-colors", "focus:ring-2"], { variants: { focusRing: { blue: ["focus:ring-blue-500"] } }, defaultVariants: { focusRing: "blue" }, }); const buttonVariants = cva(["font-semibold", "rounded"], { variants: { intent: { primary: ["bg-blue-600"] } }, defaultVariants: { intent: "primary" }, }); // Compose with cx() type ButtonProps = VariantProps<typeof interactiveVariants> & VariantProps<typeof buttonVariants>; function button(props: ButtonProps): string { return cx( interactiveVariants({ focusRing: props.focusRing }), buttonVariants({ intent: props.intent }), ); }
</patterns>See examples/composition.md for composition and extension patterns.
<red_flags>
RED FLAGS
High Priority Issues:
- Manual variant type definitions -- Types drift from cva definition, defeating type safety. Always use
.VariantProps<typeof variants> - Conditional class logic outside cva -- Defeats purpose of centralized variant definitions. Add the condition as a variant.
- Missing
-- Calling without props returns incomplete classes. Always set defaults.defaultVariants - Nested ternaries for combined states -- Use
instead.compoundVariants - Only defining
for boolean variants --true
case should provide base/normal styles.false
Medium Priority Issues:
- Space-separated class strings instead of arrays -- arrays are more readable and maintainable
- Not using
for class merging -- manual concatenation is error-pronecx() - Duplicating variant styles across components -- compose from shared base variants
- Putting complex responsive logic in cva -- keep cva simple, handle responsiveness in CSS
Gotchas & Edge Cases:
makes all variants optional (nullable) -- use TypeScript utilities (VariantProps
) to make specific ones requiredRequired<Pick<>>
are applied AFTER regular variants -- order matters for class specificitycompoundVariants- Empty variant values (
or empty string) are valid -- useful for "no additional styles" casenull - Base classes are always applied -- you cannot conditionally remove them via variants
- Both
andclass
work inclassName
config -- pick one and be consistent (compoundVariants
in non-React contexts,class
if you prefer React conventions)className - Don't forget
forimport type
:VariantPropsimport { cva, type VariantProps }
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST define all variant options in the
object - NEVER use conditional class logic outside cva)variants
(You MUST use
to extract types - NEVER manually define variant prop types)VariantProps
(You MUST use
for initial state - NEVER rely on undefined props for defaults)defaultVariants
(You MUST use
for multi-condition styles - NEVER nest ternaries for combined states)compoundVariants
Failure to follow these rules will break type safety, create inconsistent styling, and defeat the purpose of using CVA.
</critical_reminders>