Skip to content

Managing Variant Chaos with CVA

Variant explosion in primitive components is a real problem that can quickly wreck a design system.

2 min read
CVA
Class Variance Authority
Tailwind
Design System
TypeScript
React

Variant explosion in primitive components is a real problem that can quickly wreck a design system. It usually starts small—maybe you just need a different color for a Button. But if you keep relying on long, messy stacks of className strings, your code eventually turns into a "string soup." It gets even worse when you start mixing boolean props like isPrimary with actual variant props, making it impossible to tell which rule should actually win.

Class Variance Authority (CVA) is a great way to handle this complexity. It isn't a component library itself, but rather a "typed recipe" that turns variant props into a clean string of classes. The real win here is the API honesty; by using TypeScript's VariantProps, developers don't have to guess which classes are available. Autocomplete handles the variant or size choices for you.

That said, it's not a silver bullet. Not every design change deserves a new variant. During code reviews, I try to use one standard: could this variation be used in another part of the product without needing a rename? If a prop name includes business logic or specific verbs like approveInvoice or billingButton, it shouldn't be in your primitive component. That’s the job of a domain wrapper. Don't let product-specific jargon pollute your core UI system.

When it comes to compoundVariants, only use them for design rules that actually make sense across different axes. If you find yourself needing a massive, complicated explanation for a rule, you probably don't need a new variant—you likely need an entirely different pattern component.

Also, be careful with default variants. A default value is part of your public contract. Changing a default from primary to secondary can break the look of your entire app, even if the TypeScript types don't throw an error. Treat changes to default values as a major version update.

To wrap things up, always include a className prop at the very end as an "emergency exit" for small, one-off adjustments. Just make sure you're using something like tailwind-merge so your class order stays predictable and controlled.

Managing components is all about finding the balance between flexibility and order. If you're too rigid, you'll slow everyone down; if you're too loose, you'll just drown in the chaos. Happy organizing!