DEV Community

Maulik Paghdal for Script Binary

Posted on • Originally published at scriptbinary.com

Building a Custom UI Kit with Tailwind CSS

Why Build Your Own Kit?

  • Consistency: Define styles once, use everywhere
  • Speed: No more hunting for the perfect shade or spacing
  • Maintainability: Update one place, change site-wide
  • Team alignment: Everyone uses the same components

Setting Up Your Foundation

First, extend Tailwind's default theme with your design tokens:

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        primary: {
          500: '#3b82f6',
          600: '#2563eb',
          700: '#1d4ed8',
        },
      },
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

Creating Base Component Styles

Use the @layer directive to create reusable classes:

@layer components {
  .btn {
    @apply inline-flex items-center justify-center px-4 py-2 
           font-medium rounded-lg transition-all duration-200 
           focus:outline-none focus:ring-2;
  }

  .btn-primary {
    @apply btn bg-primary-600 text-white hover:bg-primary-700;
  }

  .input {
    @apply w-full px-4 py-2 border border-gray-300 rounded-lg 
           focus:ring-2 focus:ring-primary-500;
  }
}
Enter fullscreen mode Exit fullscreen mode

Building React Components

For better flexibility, wrap your styles in components:

const Button = ({ 
  variant = 'primary', 
  size = 'md', 
  loading = false,
  children,
  ...props 
}) => {
  const variants = {
    primary: 'btn-primary',
    secondary: 'btn-secondary',
    outline: 'btn-outline',
  };

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

  return (
    <button 
      className={`btn ${variants[variant]} ${sizes[size]}`}
      disabled={loading}
      {...props}
    >
      {loading ? 'Loading...' : children}
    </button>
  );
};
Enter fullscreen mode Exit fullscreen mode

Usage:

<Button variant="primary" size="lg">Save Changes</Button>
<Button variant="outline" loading>Processing...</Button>
Enter fullscreen mode Exit fullscreen mode

Adding Dark Mode

Tailwind's dark mode support makes this easy:

@layer components {
  .btn-primary {
    @apply btn bg-primary-600 hover:bg-primary-700
           dark:bg-primary-500 dark:hover:bg-primary-600;
  }

  .card {
    @apply bg-white dark:bg-gray-800 
           border-gray-200 dark:border-gray-700;
  }
}
Enter fullscreen mode Exit fullscreen mode

Enable it in your config:

module.exports = {
  darkMode: 'class',
  // ... rest of config
}
Enter fullscreen mode Exit fullscreen mode

Key Components to Start With

Focus on these first:

  • Button: Variants (primary, secondary, outline), sizes, loading state
  • Input: Label, error state, helper text
  • Card: Header, body, footer sections
  • Alert: Success, error, warning types
  • Modal: Backdrop, close button, scroll lock

Best Practices I've Learned

Start minimal. Don't build 50 components upfront. Create what you need, when you need it.

Composition over configuration. Build smaller pieces that work together:

<Card>
  <CardHeader>User Profile</CardHeader>
  <CardBody>
    <p>Content here</p>
  </CardBody>
</Card>
Enter fullscreen mode Exit fullscreen mode

Keep accessibility in mind. Use semantic HTML, add proper ARIA labels, and test with keyboard navigation.

Document as you go. A simple README with props and examples saves time later.

Common Pitfalls

  • Over-abstracting early: Don't create a component until you've used the pattern 3+ times
  • Hardcoding colors: Always use theme colors from your config
  • Ignoring edge cases: Test long text, disabled states, and mobile views
  • Forgetting responsive design: Use Tailwind's responsive prefixes (md:, lg:)

Example: Complete Form Component

Here's a form input that handles everything:

const Input = ({ 
  label, 
  error, 
  hint,
  required,
  ...props 
}) => {
  return (
    <div className="space-y-1">
      {label && (
        <label className="block text-sm font-medium text-gray-700">
          {label}
          {required && <span className="text-red-500 ml-1">*</span>}
        </label>
      )}
      <input 
        className={error ? 'input-error' : 'input'} 
        {...props} 
      />
      {hint && !error && (
        <p className="text-sm text-gray-500">{hint}</p>
      )}
      {error && (
        <p className="text-sm text-red-600">{error}</p>
      )}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

The Result

After setting this up, my development speed increased significantly. No more decision fatigue about spacing or colors. Everything is consistent, and changes are easy.

Want to Learn More?

This is just scratching the surface. I wrote a comprehensive guide that covers:

  • Advanced component patterns
  • Form systems with validation
  • Managing component complexity
  • Storybook integration
  • Real-world examples and code

👉 Read the full article: Building a Custom UI Kit with Tailwind CSS

The complete guide includes production-ready code examples, architectural decisions, and lessons learned from building multiple UI kits.


Have you built your own UI kit? What challenges did you face? Let me know in the comments!

Top comments (1)

Collapse
 
a-k-0047 profile image
ak0047

Thanks for sharing, I’ll give it a try!