Creating a Design System with React + Tailwind
Introduction
A well-structured design system ensures consistency across your application. This guide shows how to build reusable components with React and Tailwind CSS.
Prerequisites
- React project setup
- Tailwind CSS installed
Step 1: Install Dependencies
npm install clsx tailwind-variants
Step 2: Create Base Button Component
Create components/ui/Button.tsx
:
import { tv } from 'tailwind-variants';
import { ButtonHTMLAttributes } from 'react';
const button = tv({
base: 'font-semibold rounded-lg transition-colors focus:outline-none focus:ring-2',
variants: {
intent: {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
},
size: {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
},
},
defaultVariants: {
intent: 'primary',
size: 'md',
},
});
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
intent?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
}
export default function Button({
intent,
size,
className,
children,
...props
}: ButtonProps) {
return (
<button className={button({ intent, size, className })} {...props}>
{children}
</button>
);
}
Step 3: Create Input Component
Create components/ui/Input.tsx
:
import { tv } from 'tailwind-variants';
import { InputHTMLAttributes } from 'react';
const input = tv({
base: 'border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 transition-colors',
variants: {
variant: {
default: 'border-gray-300 focus:ring-blue-500 focus:border-blue-500',
error: 'border-red-500 focus:ring-red-500 focus:border-red-500',
},
},
defaultVariants: {
variant: 'default',
},
});
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
variant?: 'default' | 'error';
label?: string;
error?: string;
}
export default function Input({
variant,
label,
error,
className,
...props
}: InputProps) {
return (
<div className="space-y-1">
{label && (
<label className="block text-sm font-medium text-gray-700">
{label}
</label>
)}
<input
className={input({ variant: error ? 'error' : variant, className })}
{...props}
/>
{error && (
<p className="text-sm text-red-600">{error}</p>
)}
</div>
);
}
Step 4: Create Card Component
Create components/ui/Card.tsx
:
import { tv } from 'tailwind-variants';
import { HTMLAttributes } from 'react';
const card = tv({
slots: {
base: 'bg-white rounded-lg border border-gray-200 shadow-sm',
header: 'px-6 py-4 border-b border-gray-200',
body: 'px-6 py-4',
footer: 'px-6 py-4 border-t border-gray-200 bg-gray-50',
},
});
const { base, header, body, footer } = card();
interface CardProps extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
function Card({ className, children, ...props }: CardProps) {
return (
<div className={base({ className })} {...props}>
{children}
</div>
);
}
function CardHeader({ className, children, ...props }: CardProps) {
return (
<div className={header({ className })} {...props}>
{children}
</div>
);
}
function CardBody({ className, children, ...props }: CardProps) {
return (
<div className={body({ className })} {...props}>
{children}
</div>
);
}
function CardFooter({ className, children, ...props }: CardProps) {
return (
<div className={footer({ className })} {...props}>
{children}
</div>
);
}
Card.Header = CardHeader;
Card.Body = CardBody;
Card.Footer = CardFooter;
export default Card;
Step 5: Create Component Index
Create components/ui/index.ts
:
export { default as Button } from "./Button";
export { default as Input } from "./Input";
export { default as Card } from "./Card";
Step 6: Usage Example
import { Button, Input, Card } from '@/components/ui';
export default function Home() {
return (
<div className="p-8 space-y-6">
<Card>
<Card.Header>
<h2 className="text-xl font-semibold">Design System Demo</h2>
</Card.Header>
<Card.Body>
<div className="space-y-4">
<Input label="Email" placeholder="Enter your email" />
<div className="flex gap-2">
<Button intent="primary">Primary</Button>
<Button intent="secondary">Secondary</Button>
<Button intent="danger" size="sm">Delete</Button>
</div>
</div>
</Card.Body>
</Card>
</div>
);
}
Summary
Using tailwind-variants with React creates maintainable, type-safe design systems with consistent styling and flexible component APIs.