Skip to content

Tailwind CSS Custom Components — Building Reusable UI Patterns

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Tailwind CSS Custom Components. We cover key concepts, practical examples, and best practices to help you master this topic.

Tailwind CSS custom components are built by composing utility classes into reusable patterns -- extracted as framework components (React, Vue, Blade) or using @apply for CSS-level component classes.

What You'll Learn

You will learn how to design reusable component systems with Tailwind, extract repeated patterns into framework components or @apply, manage component variants, and maintain consistency.

Why It Matters

Reusable components prevent code duplication. DodaTech's component library uses Tailwind utilities for every element, with framework-level components for buttons, cards, modals, and badges.

Real-World Use

Durga Antivirus Pro has 30+ reusable components (Button, Card, Badge, Modal, Table, Input) all built with Tailwind utilities. Each component supports variants via props that change utility classes.

flowchart LR
    A[Forms] --> B[Components]
    B --> C[Design System]
    B --> D[Variants]
    B --> E[Composition]
    B --> F[Framework]
    style B fill:#38bdf8,stroke:#0284c7,color:#fff
    style C fill:#22c55e,stroke:#16a34a,color:#fff

Button Component (React)

// Button.jsx
function Button({ variant = 'primary', size = 'md', children, disabled, ...props }) {
  const base = 'inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2';

  const variants = {
    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-400',
    danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
    ghost: 'text-gray-700 hover:bg-gray-100 focus:ring-gray-400',
  };

  const sizes = {
    sm: 'px-3 py-1.5 text-sm',
    md: 'px-4 py-2 text-base',
    lg: 'px-6 py-3 text-lg',
  };

  return (
    <button
      className={`${base} ${variants[variant]} ${sizes[size]} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
      disabled={disabled}
      {...props}>
      {children}
    </button>
  );
}
<Button variant="primary" size="md">Primary</Button>
<Button variant="secondary" size="lg">Secondary Large</Button>
<Button variant="danger" size="sm" disabled>Disabled Danger</Button>
<Button variant="ghost">Ghost Button</Button>

Expected output: Four buttons with different visual styles (primary, secondary, danger, ghost) and sizes (sm, md, lg) using the same component.

Card Component (React)

// Card.jsx
function Card({ children, className = '', hover = false, padding = 'md' }) {
  const paddings = { sm: 'p-4', md: 'p-6', lg: 'p-8' };

  return (
    <div className={`
      bg-white rounded-xl border border-gray-200 shadow-sm
      ${hover ? 'hover:shadow-md hover:border-gray-300 transition-all duration-200' : ''}
      ${paddings[padding]}
      ${className}
    `}>
      {children}
    </div>
  );
}

function CardHeader({ children }) {
  return <div className="border-b border-gray-100 pb-4 mb-4">{children}</div>;
}

function CardBody({ children }) {
  return <div>{children}</div>;
}

function CardFooter({ children }) {
  return <div className="border-t border-gray-100 pt-4 mt-4">{children}</div>;
}
<Card hover padding="lg">
  <CardHeader><h3 class="text-xl font-bold">Card Title</h3></CardHeader>
  <CardBody><p class="text-gray-600">Card content goes here with proper spacing.</p></CardBody>
  <CardFooter><button class="text-blue-600 hover:text-blue-800">Action</button></CardFooter>
</Card>

Expected output: A card with header, body, footer sections, hover shadow effect, and consistent internal spacing.

Badge Component (React)

// Badge.jsx
function Badge({ variant = 'default', size = 'md', children }) {
  const variants = {
    default: 'bg-gray-100 text-gray-800',
    success: 'bg-green-100 text-green-800',
    error: 'bg-red-100 text-red-800',
    warning: 'bg-yellow-100 text-yellow-800',
    info: 'bg-blue-100 text-blue-800',
    purple: 'bg-purple-100 text-purple-800',
  };

  const sizes = {
    sm: 'px-2 py-0.5 text-xs',
    md: 'px-2.5 py-0.5 text-sm',
    lg: 'px-3 py-1 text-base',
  };

  return (
    <span className={`inline-flex items-center font-medium rounded-full ${variants[variant]} ${sizes[size]}`}>
      {children}
    </span>
  );
}
<Badge variant="success">Active</Badge>
<Badge variant="error">Blocked</Badge>
<Badge variant="warning">Pending</Badge>
<Badge variant="info">Info</Badge>
<Badge variant="purple" size="lg">Custom</Badge>

Expected output: Five badges with different semantic colors and sizes, using the same component with variant prop.

// Modal.jsx
function Modal({ isOpen, onClose, title, children }) {
  if (!isOpen) return null;

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center">
      {/* Backdrop */}
      <div className="fixed inset-0 bg-black/50 backdrop-blur-sm"
           onClick={onClose}></div>

      {/* Modal */}
      <div className="relative bg-white rounded-xl shadow-2xl w-full max-w-md mx-4 p-6 z-10
                      animate-slide-in">
        {/* Header */}
        <div className="flex items-center justify-between mb-4">
          <h3 className="text-lg font-bold text-gray-900">{title}</h3>
          <button onClick={onClose}
                  className="text-gray-400 hover:text-gray-600 transition-colors">
            <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
            </svg>
          </button>
        </div>

        {/* Content */}
        <div className="text-gray-600">{children}</div>

        {/* Footer */}
        <div className="flex justify-end gap-3 mt-6 pt-4 border-t border-gray-100">
          <button onClick={onClose}
                  className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors">
            Cancel
          </button>
          <button className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors">
            Confirm
          </button>
        </div>
      </div>
    </div>
  );
}

Expected output: A modal dialog with backdrop overlay, close button, title, content area, and action buttons. Appears with slide-in animation.

Component Composition

// Using composed components
function UserProfileCard({ user }) {
  return (
    <Card hover>
      <div className="flex items-center gap-4">
        <div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center text-blue-600 font-bold">
          {user.initials}
        </div>
        <div className="flex-1">
          <h3 className="font-bold text-gray-900">{user.name}</h3>
          <p className="text-sm text-gray-500">{user.role}</p>
        </div>
        <Badge variant={user.status === 'active' ? 'success' : 'error'}>
          {user.status}
        </Badge>
      </div>
    </Card>
  );
}

Expected output: A user profile card composed from Card, Badge, and inline utility components, demonstrating component composition.

Common Mistakes

1. Mixing Component Abstractions

Using both @apply and framework components for the same purpose creates confusion. Pick one pattern and use it consistently.

2. Not Accepting className Prop

Components must accept and merge a className prop for customization. Without it, users cannot override styles externally.

3. Hardcoding Styles in Components

Making assumptions about colors or sizes that should be configurable via props reduces component reusability.

4. Forgetting Tailwind IntelliSense in JSX

VS Code's Tailwind IntelliSense works in JSX files. Ensure you have the extension installed for autocomplete in template literals.

5. Over-Abstracting Too Early

Build components with inline utilities first. Extract to a reusable component only when the pattern appears 3+ times.

Practice Questions

  1. What is the recommended approach for reusable components? Framework-level components (React, Vue, Blade) that compose Tailwind utilities via props.

  2. How do you handle component variants? Use a variants object mapping variant names to Tailwind class strings, selected via a prop.

  3. What prop should every component accept? className (or class in Vue) to allow external style overrides.

  4. When should you use @apply vs framework components? @apply for global component styles (third-party overrides). Framework components for application-specific reusable patterns.

  5. How do you make a component accessible? Use semantic HTML, aria-* attributes, role attributes, focus management, and sr-only for hidden labels.

Challenge

Build a complete component set: Button (5 variants, 3 sizes), Card (with header/body/footer), Badge (6 variants), Modal (with backdrop, animation, keyboard close), and Input (with label, error state, icon slot).

FAQ

Can Tailwind components be used with any framework?

Yes. The underlying CSS is framework-agnostic. Framework-specific components are wrappers that manage utility classes via props.

How do I document Tailwind components?

Use Storybook with Tailwind support, or create a visual component library page within the project.

Should I use @apply for every component?

No. @apply is for global, framework-agnostic components. Use framework components (React/Vue) for application-specific patterns.

How do I handle dark mode in components?

Include dark: variants in the component's tailwind classes. For framework components, detect theme via context or prop.

Can I use Tailwind with CSS-in-JS libraries?

Yes. Tailwind works alongside CSS-in-JS. Use the class composition pattern within styled components or emotion.

Mini Project

Build a complete component library with: Button (5 variants x 3 sizes + icon + loading state), Card (with variants: default, interactive, highlighted), Badge (6 variants + dot indicator), Modal (with sizes, animations, trap focus), and Input (with label, helper, error, icon).

What's Next

Now master Optimization techniques for production builds. Learn purging, JIT configuration, bundle analysis, and performance tuning for Tailwind projects.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro