Variações de componentes com CVA
Use a biblioteca class-variance-authority para definir APIs de variações tipadas para componentes React, mantendo a lógica das classes limpa e previsível.
Variações de componentes com CVA é uma aula grátis de Tailwind CSS Academy no CoddyKit. Esta é a aula 3 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de Tailwind CSS Academy, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de Tailwind CSS Academy inclui 4 aulas no total.
Partes desta aula ainda não foram traduzidas e aparecem em inglês.
The Problem With Manual Variant Logic
Building component variant systems manually — with nested if statements and class maps — scales poorly. A button with 3 colors, 3 sizes, and a disabled state requires 18+ unique class combinations to handle correctly. The logic becomes sprawling and error-prone. Class Variance Authority (CVA) provides a structured, type-safe API for defining variant class maps, reducing variant logic to a clean declarative configuration.
Installing and Importing CVA
Install class-variance-authority with npm and import the cva function into your component file. CVA works alongside your existing cn() helper — CVA handles the variant logic, and tailwind-merge handles conflict resolution when caller classes are merged in. This combination is the foundation of libraries like shadcn/ui.
# Install
npm install class-variance-authority
# Also install companions if not present
npm install clsx tailwind-merge
// lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// In your component file
import { cva, type VariantProps } from 'class-variance-authority';Defining a Component With cva
Call cva() with two arguments: a string of base classes that always apply, and an options object with a variants map. Each key in variants is a prop name, and its value is an object mapping prop values to class strings. CVA merges the base classes with the matched variant classes when the returned function is called with prop values.
import { cva } from 'class-variance-authority';
const buttonVariants = cva(
// Base classes — always applied
'inline-flex items-center justify-center rounded-lg font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:opacity-50 disabled:pointer-events-none',
{
variants: {
variant: {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
outline: 'border border-gray-300 bg-transparent hover:bg-gray-50',
ghost: 'hover:bg-gray-100 text-gray-700'
},
size: {
sm: 'h-8 px-3 text-xs',
md: 'h-10 px-4 text-sm',
lg: 'h-12 px-6 text-base'
}
},
defaultVariants: {
variant: 'primary',
size: 'md'
}
}
);Using VariantProps for TypeScript
CVA exports the VariantProps utility type that automatically infers the correct TypeScript types for your variant props from the cva definition. This means TypeScript will error if you pass an invalid variant value — like variant='purple' when only primary, secondary, outline, and ghost are defined. You get full IntelliSense autocomplete for variant values in your editor.
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva('...', { variants: { /* ... */ } });
// VariantProps extracts the type automatically
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
function Button({ variant, size, className, ...props }: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
);
}Compound Variants
CVA supports compound variants — classes that apply only when a specific combination of variant values is active. For example, you might want a special style that applies only when both variant='primary' and size='lg' are used together. This is more expressive than writing nested conditionals and keeps all variant logic in one place.
const buttonVariants = cva('base-classes', {
variants: {
variant: {
primary: 'bg-blue-600 text-white',
destructive: 'bg-red-600 text-white'
},
size: {
sm: 'h-8 px-3 text-xs',
lg: 'h-12 px-6 text-base'
}
},
compoundVariants: [
{
// Apply only when BOTH conditions are true
variant: 'primary',
size: 'lg',
class: 'shadow-lg hover:shadow-xl'
},
{
variant: 'destructive',
size: 'lg',
class: 'ring-2 ring-red-300'
}
]
});Default Variants
The defaultVariants field in the cva options specifies which variant values are used when the corresponding prop is not provided. This makes most props optional while still producing correct output. A Button component with defaultVariants: { variant: 'primary', size: 'md' } renders a medium-sized primary button when used as <Button> with no props.
const badgeVariants = cva(
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
{
variants: {
variant: {
default: 'bg-gray-100 text-gray-800',
success: 'bg-green-100 text-green-800',
warning: 'bg-yellow-100 text-yellow-800',
danger: 'bg-red-100 text-red-800'
}
},
defaultVariants: {
variant: 'default' // <Badge /> without variant prop → default style
}
}
);
// All three are valid:
<Badge /> // uses default variant
<Badge variant='success' /> // green
<Badge variant='danger' /> // redCVA for Non-Button Components
CVA is not limited to buttons — use it for any component with multiple style variants. Cards can have elevated, outlined, and ghost variants. Inputs can have default and error states. Text elements can have h1 through h6 size variants. CVA's declarative API works equally well for any component where CSS classes vary based on props.
const cardVariants = cva(
'rounded-xl overflow-hidden',
{
variants: {
variant: {
elevated: 'bg-white shadow-md hover:shadow-lg transition-shadow',
outlined: 'bg-white border border-gray-200',
filled: 'bg-gray-50'
},
padding: {
none: '',
sm: 'p-4',
md: 'p-6',
lg: 'p-8'
}
},
defaultVariants: {
variant: 'elevated',
padding: 'md'
}
}
);Composing CVA Variants
Multiple CVA definitions can be composed together. A base button style can be defined with cva, and a more specific IconButton can call the base cva function and merge its result with icon-specific classes. This composition model mirrors component composition in React and keeps each variant definition focused on a single concern.
// Base shared button classes
const baseButton = cva(
'inline-flex items-center justify-center font-medium transition-colors disabled:opacity-50',
{
variants: {
variant: {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
ghost: 'hover:bg-gray-100'
}
}
}
);
// Icon button extends base
function IconButton({ icon, variant, 'aria-label': label }) {
return (
<button
className={cn(
baseButton({ variant }),
'h-10 w-10 rounded-full p-0' // icon-specific additions
)}
aria-label={label}
>
{icon}
</button>
);
}Exporting Variants for Reuse
In component libraries, it is common to export the cva function result (the variants function) alongside the component itself. This allows other components to apply the same variant classes without importing the full component. For example, a Link component might want to apply buttonVariants({ variant: 'primary' }) to render as a button visually while remaining a semantic anchor element.
// components/Button.tsx
export const buttonVariants = cva('...base...', {
variants: {
variant: {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-100 text-gray-900'
},
size: { sm: 'h-8 px-3', md: 'h-10 px-4', lg: 'h-12 px-6' }
}
});
export function Button({ variant, size, className, ...props }) {
return <button className={cn(buttonVariants({ variant, size }), className)} {...props} />;
}
// In another file: Link styled as a button
import { buttonVariants } from './Button';
<a href='/signup' className={cn(buttonVariants({ variant: 'primary', size: 'md' }))}>
Sign Up
</a>CVA With Responsive Variants
Tailwind's responsive prefixes can be included inside CVA class strings just like any other utility. This means you can define variants that include responsive behavior. For example, a layout variant might apply flex-col md:flex-row — the CVA system does not need to know about Tailwind's breakpoints; they are just class strings.
const stackVariants = cva(
'flex gap-4',
{
variants: {
direction: {
vertical: 'flex-col',
horizontal: 'flex-row',
responsive: 'flex-col sm:flex-row' // breakpoint in variant
},
align: {
start: 'items-start',
center: 'items-center',
end: 'items-end'
}
},
defaultVariants: {
direction: 'vertical',
align: 'start'
}
}
);
// Usage
<div className={stackVariants({ direction: 'responsive', align: 'center' })}>
{/* Children stack vertically on mobile, horizontally on sm+ */}
</div>Real-World CVA Component Library
A mature CVA setup defines all primitive components — Button, Badge, Input, Card, Alert — each with their own cva definitions. These components are collected into a components/ui folder (as seen in shadcn/ui), each file exporting both the React component and its variants function. This structure makes the variant system discoverable, testable, and easy to extend.
// components/ui/alert.tsx
import { cva, type VariantProps } from 'class-variance-authority';
export const alertVariants = cva(
'relative w-full rounded-lg border p-4 text-sm',
{
variants: {
variant: {
default: 'bg-white text-gray-900 border-gray-200',
destructive: 'bg-red-50 text-red-800 border-red-200',
success: 'bg-green-50 text-green-800 border-green-200'
}
},
defaultVariants: { variant: 'default' }
}
);
export function Alert({ variant, className, children, ...props }) {
return (
<div className={cn(alertVariants({ variant }), className)} role='alert' {...props}>
{children}
</div>
);
}Quick Check
Test your understanding of Tailwind CSS Mastery concepts from this lesson.
Lesson Recap
In this lesson you learned: cva() defines variant maps with base classes plus conditional class groups, VariantProps automatically derives TypeScript types from the cva definition, and compoundVariants apply classes only when specific combinations of variants are active. Next up we explore how tailwind-merge prevents class conflicts when composing components.
Perguntas Frequentes
A aula “Variações de componentes com CVA” é grátis?
Sim — o texto completo de “Variações de componentes com CVA” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de Tailwind CSS Academy, atualize para CoddyKit PRO. O curso de Tailwind CSS Academy inclui 4 aulas no total.
O que vou aprender em “Variações de componentes com CVA”?
Use a biblioteca class-variance-authority para definir APIs de variações tipadas para componentes React, mantendo a lógica das classes limpa e previsível. Você pratica Tailwind CSS Academy com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.
Preciso ter experiência prévia para começar Tailwind CSS Academy?
Nenhuma experiência prévia é necessária. Tailwind CSS Academy no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 3 de 4.
Quanto tempo leva a aula “Variações de componentes com CVA”?
A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.
Posso escrever e executar código nesta aula de Tailwind CSS Academy?
Sim. Cada aula de Tailwind CSS Academy inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.
Todas as aulas deste curso
- Configurando o Tailwind no Next.js
- Classes condicionais no React
- Variações de componentes com CVA
- Evitando conflitos entre classes com tailwind-merge