CVA를 사용한 컴포넌트 변형
class-variance-authority 라이브러리를 사용하여 React 컴포넌트에 타입이 지정된 변형 API를 정의하고, 클래스 로직을 깔끔하고 예측 가능하게 유지합니다.
CVA를 사용한 컴포넌트 변형은(는) CoddyKit의 무료 Tailwind CSS Academy 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Tailwind CSS Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Tailwind CSS Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
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.
자주 묻는 질문
“CVA를 사용한 컴포넌트 변형” 강의는 무료인가요?
네 — “CVA를 사용한 컴포넌트 변형” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Tailwind CSS Academy 강의 전체를 잠금 해제할 수 있습니다. Tailwind CSS Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“CVA를 사용한 컴포넌트 변형”에서 뭘 배우나요?
class-variance-authority 라이브러리를 사용하여 React 컴포넌트에 타입이 지정된 변형 API를 정의하고, 클래스 로직을 깔끔하고 예측 가능하게 유지합니다. 브라우저에서 직접 실행하는 실습 코드로 Tailwind CSS Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Tailwind CSS Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Tailwind CSS Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.
“CVA를 사용한 컴포넌트 변형” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Tailwind CSS Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Tailwind CSS Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- Next.js에서 Tailwind 설정하기
- React의 조건부 클래스
- CVA를 사용한 컴포넌트 변형
- tailwind-merge로 클래스 충돌 방지