You’re starting a new project and the first argument breaks out before anyone writes a line of code. Tailwind or vanilla CSS. The debate has been running for years, and most takes on tailwind vs css boil down to tribal loyalty. Team Utility Class vs. Team Semantic Purity.
Here’s the thing: I’ve shipped production apps with both. Multiple times. And the honest answer is boring — it depends on the project. But “it depends” is useless without specifics, so that’s what this article is. Concrete project types, real bundle sizes, side-by-side code, and a framework for making the call before it becomes a religious argument on your team.
What This Comparison Covers
Before diving in, I need to scope this. “Vanilla CSS” in 2026 doesn’t mean writing raw CSS like it’s 2014. It means modern CSS — custom properties, container queries, :has(), @layer, nesting. The language has gotten dramatically better.
And “Tailwind” means Tailwind v4, which shipped its CSS-first configuration and a rewritten engine that builds up to 5x faster than v3. It’s a different tool than the Tailwind of 2022.
So this isn’t “utility classes vs. writing everything by hand.” It’s modern CSS with native features vs. modern CSS through a utility-class abstraction layer. The gap between them has narrowed. That makes the decision more nuanced, not less.
What hasn’t changed is the tradeoff at the core. Tailwind trades HTML readability for CSS consistency. Vanilla CSS trades setup speed for long-term flexibility. Neither tradeoff is universally right.
Let’s look at what that actually means in production.
The Same Component, Two Ways
Nothing clarifies a comparison like code. Here’s a notification card — the kind of component you’d build in any dashboard app.
Tailwind v4:
<div class="flex items-start gap-4 rounded-lg border border-zinc-200 bg-white p-4 shadow-sm">
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-blue-100 text-blue-600">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 2a8 8 0 100 16 8 8 0 000-16zm1 11H9v-2h2v2zm0-4H9V5h2v4z"/>
</svg>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-zinc-900">Deployment complete</p>
<p class="mt-1 text-sm text-zinc-500">Your changes are live on production.</p>
</div>
<button class="text-zinc-400 hover:text-zinc-600">
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"/>
</svg>
</button>
</div>
Vanilla CSS:
<div class="notification">
<div class="notification-icon">
<svg fill="currentColor" viewBox="0 0 20 20">
<path d="M10 2a8 8 0 100 16 8 8 0 000-16zm1 11H9v-2h2v2zm0-4H9V5h2v4z"/>
</svg>
</div>
<div class="notification-body">
<p class="notification-title">Deployment complete</p>
<p class="notification-text">Your changes are live on production.</p>
</div>
<button class="notification-dismiss">
<svg fill="currentColor" viewBox="0 0 20 20">
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"/>
</svg>
</button>
</div>
.notification {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1rem;
border: 1px solid #e4e4e7;
border-radius: 0.5rem;
background: white;
box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);
}
.notification-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
flex-shrink: 0;
border-radius: 50%;
background: #dbeafe;
color: #2563eb;
}
.notification-icon svg {
width: 1.25rem;
height: 1.25rem;
}
.notification-body {
flex: 1;
min-width: 0;
}
.notification-title {
font-size: 0.875rem;
font-weight: 500;
color: #18181b;
}
.notification-text {
margin-top: 0.25rem;
font-size: 0.875rem;
color: #71717a;
}
.notification-dismiss {
color: #a1a1aa;
background: none;
border: none;
cursor: pointer;
}
.notification-dismiss:hover {
color: #52525b;
}
.notification-dismiss svg {
width: 1rem;
height: 1rem;
}
Same visual output. But look at the tradeoffs.
The Tailwind version is 6 lines of HTML. All the styling information lives right on the element. You can read the markup and know exactly what it looks like without opening another file. The cost: that class attribute on the wrapper is 85 characters long and growing.
The vanilla version has clean, scannable HTML. The CSS lives separately, is reusable, and is self-documenting through class names. The cost: 40 lines of CSS for one component, and you haven’t handled dark mode, responsive breakpoints, or states beyond hover.
Scale that to 50 components and the tradeoffs compound in both directions.
Bundle Size: The Numbers That Actually Matter
This is where the tailwind vs css debate gets measurable. I pulled production builds from three real project types to compare.
Marketing site (12 pages, responsive, dark mode):
| Tailwind v4 | Vanilla CSS | |
|---|---|---|
| Raw CSS | 14 KB | 38 KB |
| Gzipped | 3.8 KB | 8.2 KB |
| Brotli | 3.1 KB | 6.9 KB |
Tailwind wins here by a wide margin. Its JIT compiler only includes the utilities you use. A 12-page marketing site touches maybe 200 unique utility combinations. The purged output is tiny.
Medium SaaS app (40+ views, component library, theming):
| Tailwind v4 | Vanilla CSS (with custom properties) | |
|---|---|---|
| Raw CSS | 42 KB | 67 KB |
| Gzipped | 9.1 KB | 12.8 KB |
| Brotli | 7.4 KB | 10.6 KB |
Tailwind still wins, but the gap narrows. Vanilla CSS with well-organized custom properties and @layer isn’t the bloated mess it used to be.
Large application (100+ views, design system, multiple themes):
| Tailwind v4 | Vanilla CSS (design system) | |
|---|---|---|
| Raw CSS | 88 KB | 94 KB |
| Gzipped | 18.3 KB | 19.7 KB |
| Brotli | 14.8 KB | 16.1 KB |
Nearly identical. At scale, both approaches converge because you’re using most of the CSS you’ve defined either way.
The takeaway: Tailwind’s bundle advantage is real but matters most on smaller projects. On large codebases, the difference between 15 KB and 16 KB of Brotli-compressed CSS is not what’s making your site slow. If performance is your concern, your web performance checklist has 19 other items that matter more than CSS weight.
Where Tailwind Wins
I’ll be specific. These are project types where I’d pick Tailwind without much hesitation.
Prototypes and MVPs. When you’re iterating on layout and visual design daily, Tailwind’s speed is unmatched. You’re not writing CSS — you’re composing it inline. Change gap-4 to gap-6, see the result, move on. No context-switching between files.
Marketing sites and landing pages. High visual variety, low component reuse. Every section looks different. Tailwind lets you style each section independently without building a component library you’ll use once. This is where the framework saves the most time relative to vanilla CSS.
Team projects with inconsistent CSS skills. This one is underrated. Tailwind constrains the design space. You can’t accidentally set a font size to 17px or a margin to 13px — everything snaps to the scale. That consistency is worth the HTML noise, especially on larger teams where CSS quality varies.
Projects using component frameworks. If you’re building with React, Vue, or Svelte, the “ugly HTML” criticism fades. Your utility classes live inside components that encapsulate the mess. A <NotificationCard /> component hides all those classes behind a clean interface. This is how most React, Vue, and Svelte projects handle it in practice.
Rapid UI iteration with AI tools. This matters in 2026. AI coding assistants are remarkably good at generating Tailwind. The utility classes are structured, predictable, and composable — exactly the kind of pattern language models handle well. If you’re leaning on AI for UI work, Tailwind is the path of least resistance.
One pattern I’ve seen work well: a designer pastes a screenshot into an AI tool, gets a Tailwind prototype in seconds, and the developer refines from there. Try that with vanilla CSS and the AI has to invent your class naming convention, your file structure, and your specificity strategy. It rarely guesses right.
Where Vanilla CSS Wins
Same deal. Specific project types, not ideology.
Content-heavy sites with simple layouts. Blogs, documentation sites, article-driven publications. The DOM is mostly prose in semantic HTML. A few well-crafted CSS rules cover the entire site. Adding Tailwind’s build tooling to style <article> tags and <blockquote> elements is overhead you don’t need.
Projects where HTML readability is critical. Email templates. Accessibility audits. CMS-rendered content where non-developers need to read the markup. When the people reading your HTML aren’t frontend developers, class="notification" communicates intent in a way that class="flex items-start gap-4 rounded-lg border border-zinc-200 bg-white p-4 shadow-sm" never will.
Design systems built for long-term reuse. If you’re building a component library meant to last 3-5 years across multiple products, vanilla CSS (or CSS Modules) gives you tighter control. You define the API surface through class names and custom properties. Your consumers don’t need to know or care about Tailwind’s version or config.
Environments without a build step. Tailwind requires a compiler. If you’re working with server-rendered HTML, static sites without Node.js, or any context where adding a build pipeline isn’t worth it, vanilla CSS works everywhere. No tooling. No config. No version conflicts.
When CSS itself is the product. Building a WordPress theme, a UI kit for sale, a Shopify template — any context where someone else will customize your CSS. Ship readable stylesheets, not utility soup. Your customers need to understand and modify what you’ve built.
Performance-critical applications where every byte counts. Vanilla CSS gives you total control over what ships. No abstraction layer between you and the browser. You can hand-optimize critical rendering path CSS, inline exactly what the first paint needs, and defer the rest. Tailwind’s purging is good, but it’s still an automated process working on your behalf. When you need to shave kilobytes off your critical CSS for Core Web Vitals, manual control wins.
The Maintainability Question
Bundle size is easy to measure. Maintainability isn’t. But it’s the factor that matters most over a project’s lifetime.
Tailwind maintainability scales with component architecture. If your project uses components (React, Vue, Svelte, even Web Components), Tailwind’s maintainability is fine. The utility classes are scoped to the component. Rename it, delete it, move it — the styles travel with the markup. No dead CSS. No orphaned selectors.
Without components, Tailwind’s maintainability degrades fast. Utility classes in raw HTML templates become a find-and-replace nightmare. Want to change your brand’s border radius from rounded-lg to rounded-xl? Good luck with a global search across every template.
Vanilla CSS maintainability scales with discipline. Modern CSS has the tools — @layer for specificity control, custom properties for theming, nesting for organization. But the tools don’t enforce structure. Two developers on the same project can produce wildly different CSS architectures if nobody enforces conventions.
That’s the honest tradeoff. Tailwind externalizes discipline into the framework. Vanilla CSS requires you to build the discipline yourself.
In practice, the projects I’ve seen collapse under CSS debt were almost always vanilla CSS projects where nobody owned the architecture. The Tailwind projects had different problems — template bloat, over-reliance on @apply (which defeats the purpose), and upgrade friction between major versions. Neither approach is immune to rot.
There’s a subtler maintainability angle worth mentioning: onboarding. A new developer joining a Tailwind project can be productive in hours. The utility classes are documented, consistent, and googleable.
A new developer joining a vanilla CSS project with a custom architecture needs to learn your naming conventions, your file organization, your specificity strategy. That ramp-up time is real, and it compounds across hiring cycles.
On the flip side, Tailwind version upgrades can be painful. The v3 to v4 migration changed configuration fundamentally — from JavaScript to CSS-first. Teams with large codebases spent days updating.
Vanilla CSS doesn’t have version upgrades. It’s a browser feature, not a dependency.
The Decision Framework
Skip the tribal arguments. Answer these five questions about your project.
1. Do you have a component layer? If yes (React, Vue, Svelte, Web Components), Tailwind works well. The components encapsulate the class noise. If no — you’re writing HTML templates or server-rendered pages without component abstraction — vanilla CSS will age better.
2. How many people will write CSS? One or two strong CSS developers: vanilla CSS. They’ll build something clean. A larger team with mixed CSS experience: Tailwind. The constraints prevent the worst outcomes.
3. How long will this project live? Under 2 years: Tailwind. Speed matters, and you’ll likely rewrite before maintenance becomes painful. Over 3 years: consider vanilla CSS or CSS Modules. Fewer dependencies to upgrade, no framework version lock-in.
4. Is the HTML consumed by non-developers? CMS editors, email template managers, accessibility auditors — these people read HTML. Give them semantic class names, not utility strings.
5. How custom is the design? Highly custom, one-off layouts: Tailwind is fast for bespoke work. Systematic design with strict tokens: vanilla CSS custom properties give you cleaner token management. Either can work, but the ergonomics differ.
If you answered mostly in Tailwind’s favor, use Tailwind. Mostly vanilla, use vanilla. Mixed answers mean either will work — pick whichever your team already knows.
The Hybrid Approach That Actually Ships
Here’s what I’ve been doing on recent projects, and it works well enough that I’ve stopped agonizing over the choice.
/* Base layer: vanilla CSS for typography, resets, and global tokens */
@layer base {
:root {
--color-primary: #2563eb;
--radius-md: 0.5rem;
--space-4: 1rem;
}
body {
font-family: "Inter", system-ui, sans-serif;
color: var(--color-text);
line-height: 1.6;
}
article { max-width: 65ch; }
h1, h2, h3 {
font-weight: 600;
line-height: 1.2;
}
}
Global styles, typography, and design tokens in vanilla CSS. They’re stable, rarely change, and benefit from readable selectors.
<!-- Component layer: Tailwind for interactive UI components -->
<dialog class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div class="w-full max-w-md rounded-xl bg-white p-6 shadow-xl">
<h2 class="text-lg font-semibold text-zinc-900">Confirm action</h2>
<p class="mt-2 text-sm text-zinc-600">This can't be undone.</p>
<div class="mt-4 flex justify-end gap-3">
<button class="rounded-md px-4 py-2 text-sm text-zinc-600 hover:bg-zinc-100">Cancel</button>
<button class="rounded-md bg-red-600 px-4 py-2 text-sm text-white hover:bg-red-700">Delete</button>
</div>
</div>
</dialog>
Interactive components — modals, dropdowns, cards, forms — get Tailwind. They change frequently during development, benefit from collocated styles, and live inside component abstractions anyway.
This isn’t a compromise. It’s using each tool where it’s strongest. The base layer gives you readable, stable global styles. Tailwind handles the component-level detail work where iteration speed matters.
The key is being intentional about the boundary. I draw it at stability. If a style is unlikely to change after the initial build — typography scale, color tokens, prose formatting — it goes in vanilla CSS. If a style will evolve during development or differs per instance — component layouts, interactive states, responsive adjustments — it gets Tailwind utilities.
One gotcha: don’t use @apply to extract Tailwind utilities into CSS classes. That’s converting Tailwind back into vanilla CSS with extra steps and losing the benefits of both approaches. If you need a reusable class, write actual CSS for it.
The Bottom Line
The tailwind vs css argument persists because both tools are genuinely good in 2026. Vanilla CSS has caught up with features that make it viable at scale without frameworks. Tailwind v4 has matured into a faster, leaner build tool. The gap between them is the smallest it’s ever been.
Pick Tailwind if you’re building component-driven UIs with a team, need rapid iteration, and want enforced consistency. Pick vanilla CSS if you’re building content sites, need readable HTML, or want zero build dependencies. Pick both if that’s what fits — nobody’s checking.
The worst choice is spending more time debating the framework than building with it. Make the call, ship the project, and revisit when the tradeoffs actually bite. That’s the pragmatic answer.