Hi, I’m Prashuk jain. I spend most of my time building full-stack applications with the MERN stack and Next.js. If there’s one thing I’ve learned from building and shipping projects, it’s that your UI code can quickly become a nightmare if you don't have a solid plan for reusability from day one.
In this post, I want to talk about how I build UI components using React and Tailwind CSS—focusing on what actually works in production, not just what looks good in a tutorial.
The "Copy-Paste" Trap vs. Real Reusability
When we start a project, it’s tempting to just copy a button or a card from an old repo. But three weeks later, when the client wants the "primary" blue to be slightly darker across twenty different screens, you realize you're in trouble.
True reusability isn't just about making a component you can use twice. It’s about building a component that is flexible enough to handle edge cases but strict enough to maintain design consistency.
Why Tailwind is the Perfect Partner for React
I used to be a CSS-Modules or Styled-Components person, but Tailwind changed my workflow. The biggest advantage isn't just the speed of writing classes; it's the lack of "context switching."
When you use Tailwind with React, your logic and your styling live in the same place. This makes it much easier to see how a component's state (like isLoading or isDisabled) directly affects its visual style.
Handling Dynamic Classes the Right Way
The messiest part of Tailwind in React is string concatenation. Doing something like className={`btn ${isPrimary ? 'bg-blue-500' : 'bg-gray-500'}`} gets ugly fast.
In my projects, I always use two utility libraries: clsx and tailwind-merge.
- clsx: Helps toggle classes based on conditions.
- tailwind-merge: Ensures that if you pass a custom class to a component, it actually overrides the base Tailwind class instead of conflicting with it.
Practical Implementation: The Button Component
Let’s look at a "Real-World" Button component. I usually use a library called class-variance-authority (CVA) because it allows us to define "variants" in a very clean, structured way.
import { cva, type VariantProps } from 'class-variance-authority';
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
// Utility to merge classes
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none disabled:opacity-50 disabled:pointer-events-none",
{
variants: {
variant: {
primary: "bg-blue-600 text-white hover:bg-blue-700",
outline: "border border-gray-300 bg-transparent hover:bg-gray-100",
ghost: "bg-transparent hover:bg-gray-100 text-gray-700",
},
size: {
sm: "h-9 px-3",
md: "h-10 px-4 py-2",
lg: "h-11 px-8",
},
},
defaultVariants: {
variant: "primary",
size: "md",
},
}
);
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button = ({ className, variant, size, ...props }: ButtonProps) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
};
export default Button;
Performance and Clean Code
I care a lot about performance. One mistake I see often is people importing massive UI libraries for just one or two components. By building your own set of core components (Buttons, Inputs, Modals) using the pattern above, you keep your bundle size lean.
Also, focus on the "API" of your component. A good component should be easy to use for another developer. If they have to read 100 lines of code just to figure out how to change a color, the component has failed.
Best Practices for Your Component Library
- Don't over-abstract too early: If you only have two buttons in your whole app, you might not need a complex CVA setup. Build for what you need today, but keep the structure ready for tomorrow.
- Prop Forwarding: Always use
{...props}and forward your refs. This ensures your custom component behaves like a native HTML element. - Consistency is Key: Use a
tailwind.config.jsfile to define your brand colors and spacing. Never hardcode hex codes likebg-[#f3f3f3]inside your components.
Conclusion
Building reusable UI isn't about writing the most clever code; it's about making your future self's life easier. By combining React’s component model with Tailwind’s utility classes and a tool like CVA, you create a system that is both flexible and maintainable.
My main takeaway from years of project work is this: Spend the extra 10 minutes today to structure your base components correctly. It will save you 10 hours of debugging and refactoring next month.