Hey everyone, I’m Prashuk.
If you’ve been following my work, you know I’m not a fan of over-complicating things. I like building products that actually ship. I’ve spent way too many hours in the past fighting with bloated CSS frameworks or trying to override "pre-styled" components that just wouldn't budge.
Lately, my go-to stack for getting a clean, professional UI off the ground is Tailwind CSS paired with Headless UI. It’s a combination that just works without getting in your way.
Here’s how I think about building a design system that doesn't fall apart after two weeks.
Why I stopped using "Pre-Styled" Libraries
In one of my early projects, I used a very popular UI library (I won't name names, but we've all used it). It looked great at first. But the moment the client asked for a custom behavior—like a specific animation on a dropdown—I spent six hours just trying to override their default CSS.
I ran into this issue when the bundle size started exploding because I was importing a massive library just for a few modals and buttons.
What worked for me was moving toward a "Headless" approach. I want the logic (the keyboard navigation, the ARIA labels) handled for me, but I want 100% control over the pixels. That’s exactly what Headless UI does.
Setting the Foundation with Tailwind
A design system isn't just a folder of components; it’s a set of rules. I always start by locking down the tailwind.config.js.
I’ve learned the hard way that if you don't define your primary colors and spacing scales early, your project will eventually have 50 shades of gray (and not the good kind).
- Standardize Colors: Define
brand-primary,brand-secondary, etc. - Spacing: Stick to the default tailwind scale, but don't be afraid to extend it if your design is "breathier."
- Typography: Set your base font sizes so you aren't guessing between
text-smandtext-baseevery time.
The "Headless" Secret Sauce
Headless UI is made by the Tailwind team. It provides unstyled, accessible components like Modals, Tabs, and Popovers.
The beauty is that it handles the hard stuff. For example, when a Modal opens, it handles "focus trapping" (so the user can't tab out of the modal into the background). Doing that manually is a massive pain and usually ends up buggy.
I combine this with tailwind-merge and clsx to create really flexible components.
A Practical Example: The Reusable Modal
Here’s a quick look at how I usually wrap a Headless UI Dialog. This keeps the code clean and reusable across the entire app.
import { Dialog, Transition } from '@headlessui/react';
import { Fragment } from 'react';
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
// Helper for clean tailwind classes
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export default function BaseModal({ isOpen, onClose, title, children }) {
return (
<Transition show={isOpen} as={Fragment}>
<Dialog onClose={onClose} className="relative z-50">
{/* Backdrop */}
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel className="mx-auto max-w-sm rounded-lg bg-white p-6 shadow-xl">
<Dialog.Title className="text-lg font-bold text-slate-900">
{title}
</Dialog.Title>
<div className="mt-4">{children}</div>
</Dialog.Panel>
</div>
</Dialog>
</Transition>
);
}
Performance and Clean Code
I’m big on performance. One thing I love about this setup is that Tailwind JIT (Just-In-Time) compiler only generates the CSS you actually use.
To keep the code clean, I follow a simple rule: "Don't abstract too early."
I've seen devs create a Button component with 50 different props for every possible scenario. It becomes a nightmare to maintain. Instead, I build a base button for the 90% use case. If a specific page needs a "special" button, I just write the Tailwind classes right there. It’s okay to have a little repetition if it keeps the logic simple.
My Best Practices
- Focus on States: Always style your
:focus-visible,:hover, and:disabledstates. Tailwind makes this easy (e.g.,focus:ring-2). - Use the
cnutility: As shown in the code above, usetailwind-merge. It prevents class conflicts when you try to override a style from a parent component. - Think in Tokens: If you find yourself typing
text-[#3b82f6]more than twice, move it to your config as a named color.
Final Thoughts
Building a design system doesn't mean you need a 50-page Storybook documentation before you write your first line of feature code.
For me, it’s about building the system as I build the product. Tailwind gives me the speed, and Headless UI gives me the accessibility. This combo has saved me countless hours of debugging UI glitches, letting me focus on the actual business logic of my MERN apps.
Keep it simple, keep it performant, and don't over-engineer things until you actually need to.
Catch you in the next one! — Prashuk