Skip to content
Go back

Creating a Design System with React + Tailwind

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

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.


Share this post on:

Previous Post
Using React Query with GraphQL for Optimized Fetching
Next Post
Setting Up React 19 with Server Components