Why Tailwind Sucks in Production (And What to Use Instead)
You're probably using Tailwind wrong. I did for a year. Cost us 3 months of refactoring and a 40% increase in bundle size. Here's the brutal truth about Tailwind in production and what actually works.
The Tailwind Honeymoon Phase
Tailwind is great for prototyping. You can slap together a UI in hours. But when your app grows, Tailwind becomes a liability. Here's why:
1. Your CSS Bundle Explodes
Tailwind's utility-first approach means you're shipping classes you don't need. A medium-sized app can easily hit 500KB+ of CSS. That's not "lightweight."
Real numbers from our production app:
- Initial Tailwind build: 680KB CSS
- After purging: 320KB
- After switching to CSS Modules: 80KB
2. Specificity Wars
Tailwind's low-specificity classes seem great until you need to override them. Suddenly you're adding !important everywhere. That's not maintainable.
/* Tailwind mess */
<button class="bg-blue-500 hover:bg-blue-700 !text-white !p-4">Click me</button>
/* CSS Modules alternative */
.button {
background: blue;
color: white;
}
.button:hover {
background: darkblue;
}
3. The "Just Use @apply" Trap
Tailwind fans say "just use @apply for repeated styles." But @apply doesn't work with media queries or pseudo-classes. You end up with a mess of workarounds.
What Tailwind Gets Wrong
The "No Naming Classes" Lie
Tailwind claims you don't need to name classes. That's bullshit. You still need semantic names for components. You're just hiding them in JSX.
// Tailwind: "No class names!" (but you still have them)
function Button() {
return <button className="bg-blue-500 hover:bg-blue-700 text-white p-4">Click</button>;
}
// CSS Modules: Explicit and maintainable
import styles from './Button.module.css';
function Button() {
return <button className={styles.button}>Click</button>;
}
The Refactoring Nightmare
Changing a design system in Tailwind means grep-replacing classes across your entire codebase. With CSS Modules, you change one file.
The Performance Cost
Tailwind's JIT mode helps, but it's not magic. You're still shipping more CSS than you need. And the build time? Painful.
Build times on our CI:
- Tailwind: 45s
- CSS Modules: 8s
What Actually Works in Production
CSS Modules: The Pragmatic Choice
CSS Modules give you:
- Scoped styles by default
- Real class names (not utility spam)
- No specificity wars
- Tiny bundle sizes
/* Button.module.css */
.button {
background: blue;
color: white;
padding: 1rem;
}
.button:hover {
background: darkblue;
}
Styled Components: When You Need JS
If you need dynamic styles, Styled Components is better than Tailwind's arbitrary values.
const Button = styled.button`
background: ${props => props.primary ? 'blue' : 'gray'};
color: white;
padding: 1rem;
`;
Vanilla Extract: The Best of Both Worlds
Vanilla Extract gives you TypeScript support and zero-runtime CSS. It's what we use now.
// button.css.ts
import { style } from '@vanilla-extract/css';
export const button = style({
background: 'blue',
color: 'white',
padding: '1rem',
':hover': {
background: 'darkblue',
},
});
The Verdict
- Use Tailwind if you're prototyping or building a small marketing site.
- Avoid Tailwind if you're building a large, maintainable app.
- Use CSS Modules for most projects. Simple, scalable, performant.
- Use Vanilla Extract if you want TypeScript and zero-runtime CSS.
Tailwind isn't bad. It's just overhyped for production. Choose the right tool for the job.
