Tailwind vs vanilla CSS — the honest cost analysis.
The Tailwind-vs-vanilla debate has become religious. Both camps misrepresent the other side. The truth is that they win in different contexts — and choosing wrong costs you months of velocity. This is the working framework: where each genuinely shines, where each fails, and how to decide for your project without reading another Twitter argument.
01Where Tailwind genuinely wins
Tailwind earned its market share for real reasons. The places it wins are concrete:
- Prototype velocity. You can design a UI in HTML without ever leaving the file. No context-switching to a stylesheet, no naming, no "what should I call this." Speed-to-first-render is 3-5x faster.
- Design tokens enforced at the markup level. When everyone uses
p-4instead of choosing arbitrary pixel values, the design stays consistent automatically. The constraint is the feature. - Refactoring confidence. Deleting a component deletes its styles. No orphaned CSS rules accumulating in the codebase over years.
- Onboarding speed. New team members don't need to learn your CSS conventions. They learn Tailwind once and they're productive everywhere.
- JIT compilation gives you only what you use. Production bundles are typically 8-15 KB of CSS, regardless of project size.
These are real benefits. Anyone who tells you Tailwind users are "lazy" or "don't understand CSS" hasn't shipped a 30-person frontend codebase in the last five years.
02Where vanilla CSS genuinely wins
Vanilla CSS (or CSS Modules, or PostCSS — the "write actual CSS" camp) has its own wins:
- Complex interactions and animations. Anything beyond simple hover states. Multi-step animations, keyframes, complex selectors. Tailwind can do them, but the syntax becomes ugly and hard to read.
- Editorial / content-heavy designs. Typography hierarchies, drop caps, pull quotes, long-form reading layouts. Tailwind is verbose for content-heavy work because every paragraph needs the same classes.
- Brand-first design. When your visual identity is the product (luxury, fashion, editorial), Tailwind's standardization works against you. The differentiator is the bespoke detail.
- Small projects. A 10-page marketing site doesn't benefit from Tailwind's scaling properties. The setup cost outweighs the benefit.
- Native CSS features. Container queries, anchor positioning, view transitions, OKLCH colors — these are all CSS-first. Tailwind catches up eventually, but you're always a version behind the platform.
03Myths that confuse the decision
"Tailwind classes pollute your HTML." They do. Whether that's a problem depends on what you optimize for. If you read CSS files frequently, polluted HTML is annoying. If you read HTML files frequently to understand UI, polluted HTML is the point — all the styling is right there.
"Vanilla CSS is more performant." Almost never true in production. A Tailwind JIT bundle is typically smaller than a hand-written stylesheet because every utility is used exactly once. Vanilla wins on initial-load if you ship critical CSS inline, but that's a separate technique you can apply to either approach.
"You can't customize Tailwind." You can. The config file is its whole point. Custom color tokens, custom spacing scales, custom breakpoints — all configurable. The people who say this haven't read the docs since 2020.
"Vanilla CSS doesn't scale." It does, with discipline. CSS Modules give you scoping. BEM gives you naming conventions. Cascade Layers give you specificity control. The teams who fail at vanilla CSS at scale usually failed at any architecture at scale.
04The hybrid approach that actually works
Most production apps benefit from a hybrid:
- Tailwind for layout and spacing. Flexbox, grid, padding, margin — utility classes are perfect here. These are the most repeated patterns in any codebase.
- Component-scoped CSS for visual identity. Brand-specific styling, complex animations, typography systems — write actual CSS and import it into the component.
- CSS variables for design tokens. Both Tailwind and vanilla can read CSS custom properties. Define your colors, spacing, and typography as variables; reference them from either side.
// Card.tsx — Tailwind for layout, scoped CSS for brand identity
import styles from './Card.module.css';
export function Card({ title, body }) {
return (
<article className={`p-6 rounded-lg shadow-sm ${styles.card}`}>
<h3 className={styles.title}>{title}</h3>
<p className="mt-2 text-gray-600 leading-relaxed">{body}</p>
</article>
);
}
Layout uses Tailwind (p-6, rounded-lg, mt-2). Brand-specific styling — the card's particular shadow, the title's serif font, hover behaviors — lives in Card.module.css. Both approaches read CSS variables for color tokens.
05The decision framework
Five questions, in order:
- Is this a product or an editorial piece? Products → Tailwind. Editorial → vanilla.
- Is your team mostly designers or mostly engineers? Designers tend to prefer vanilla CSS. Engineers tend to prefer Tailwind. Match the team.
- How long will this codebase live? Short-term (under a year) → use whatever is fastest. Long-term → invest in the right architecture for your team's preferences.
- Does the design system come from a designer or a developer? Designer-led design systems usually have unique typography and animation requirements that Tailwind handles awkwardly.
- How much custom CSS will you write anyway? If the answer is "a lot," vanilla CSS is more honest. Adding 200 lines of custom CSS to a Tailwind project is a sign you picked wrong.
06Anti-patterns in both camps
Tailwind anti-pattern: using @apply for everything. This is component-extraction-via-string-manipulation, and it inherits the worst of both worlds — Tailwind's verbosity and vanilla's component sprawl. If you're using @apply for more than two utilities, just write a CSS class.
Vanilla anti-pattern: using arbitrary pixel values instead of design tokens. padding: 17px instead of padding: var(--space-md). This destroys the consistency Tailwind's constraint-based approach gives you for free.
Tailwind anti-pattern: not configuring your colors. Out-of-the-box Tailwind colors are fine for prototypes but generic for products. The tailwind.config.js file is where you replace defaults with your brand palette.
Vanilla anti-pattern: no naming convention. If your team is still debating "should we use BEM or kebab-case?" three years in, you've already lost the architecture battle. Pick anything; just be consistent.
∞The honest take
Both approaches work. Neither is universally correct. Most professional codebases I've seen use a hybrid that reflects the team's actual workflow rather than tribal allegiance. The teams that ship the best work argue about something other than CSS approach — they argue about whether the design is right, whether the code is maintainable, whether the user is served. That's the real bar.
If your team is fighting about Tailwind versus vanilla, the fight isn't really about CSS. It's about who gets to make decisions and whose workflow gets prioritized. Solve that, and the CSS choice solves itself.