Tailwind CSS vs Vanilla CSS: When Each Makes Sense
We use both Tailwind CSS and vanilla CSS across our projects. The Threshline website you are reading right now is built with vanilla CSS. Several of our client projects — including MindHyv and Trackelio — use Tailwind. We do not have a default. We choose per project based on team, timeline, and what we are building.
This post is our honest take on when each approach makes sense, based on years of shipping production code with both.
What We Mean by Vanilla CSS
When we say “vanilla CSS,” we mean modern CSS with custom properties, container queries, :has(), nesting, @layer, and the other features that have shipped in browsers over the last few years. We are not talking about writing CSS like it is 2015. Modern CSS is a different language than what most Tailwind advocates are reacting against.
/* Modern vanilla CSS — not what it used to be */
.card {
container-type: inline-size;
padding: var(--space-4);
border-radius: var(--radius-md);
background: var(--surface-primary);
& .title {
font-size: var(--text-lg);
font-weight: 600;
}
@container (min-width: 400px) {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--space-4);
}
}
Native nesting, custom properties for design tokens, container queries for component-level responsiveness. This is the baseline now. If your mental model of vanilla CSS is BEM class names and media query duplication, you are comparing against a straw man.
What Tailwind Actually Gives You
Tailwind’s real value is not “utility classes.” You could write your own utility classes in five minutes. What Tailwind gives you is:
- A design system out of the box. The spacing scale, color palette, and type scale are well-thought-out defaults. Teams that would otherwise ship inconsistent designs get guardrails for free.
- Co-location of styles and markup. You see the styling right in the template, which eliminates context switching between HTML and CSS files.
- Dead code elimination. Tailwind’s compiler only ships the classes you use. Your CSS bundle is proportional to your UI surface area, not your stylesheet complexity.
- Team velocity on large codebases. New developers can read and modify any component without needing to understand a custom CSS architecture.
Here is the same card component in Tailwind:
<div class="p-4 rounded-md bg-white @container">
<h3 class="text-lg font-semibold">Card title</h3>
<p class="text-gray-600">Card description here.</p>
<div class="@[400px]:grid @[400px]:grid-cols-[auto_1fr] @[400px]:gap-4">
<!-- responsive grid at container level -->
</div>
</div>
Both approaches produce the same result. The question is which one produces less friction for your specific team and project.

When Tailwind Wins
Application UIs with lots of interactive components. If you are building a dashboard, admin panel, or SaaS product with dozens of component variations, Tailwind’s co-location model is hard to beat. When we built the dashboard for MindHyv, Tailwind let us iterate on component layouts without managing a growing stylesheet.
Teams with mixed frontend experience. Tailwind’s class names are a shared vocabulary. A backend developer who knows p-4 means “1rem of padding” can contribute to the frontend without learning a custom CSS architecture. This is a real productivity gain on small teams.
Rapid prototyping and MVPs. When speed matters more than CSS architecture — which is most of the time in early-stage products — Tailwind lets you move fast without accumulating CSS debt. We used it heavily during the MVP phase of JustTheRip because we were iterating on layouts daily.
Projects with component libraries. If you are using a component library like shadcn/ui, Radix, or Headless UI, Tailwind integrates naturally. The component handles behavior, Tailwind handles styling, and everything stays in one file.
When Vanilla CSS Wins
Content-heavy marketing sites. Blog layouts, documentation sites, and marketing pages have a different shape than application UIs. You have long-form content that needs typographic rhythm, a limited set of layouts, and design details that benefit from fine-grained control. The Threshline site uses vanilla CSS for exactly this reason. Our stylesheet is small, easy to read, and does exactly what we need.
Projects where bundle size is critical. Tailwind’s output is small, typically 5-15 KB gzipped. But vanilla CSS for a simple site can be 2-3 KB. For sites where every kilobyte matters — landing pages, performance-critical tools — vanilla CSS has a real edge. We wrote about this in our post on building landing pages that convert.
When you need CSS features Tailwind does not expose. Custom @property animations, complex grid layouts with named areas, @layer for cascade management, advanced :has() selectors — vanilla CSS gives you the full language. Tailwind covers 90% of use cases, but the other 10% often requires dropping into arbitrary values or custom CSS anyway.
/* Things that are awkward in Tailwind but natural in CSS */
@property --gradient-angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
.animated-border {
--gradient-angle: 0deg;
background: conic-gradient(from var(--gradient-angle), #3b82f6, #8b5cf6, #3b82f6);
animation: spin-gradient 3s linear infinite;
}
@keyframes spin-gradient {
to { --gradient-angle: 360deg; }
}
Solo developers or very small teams. If you are the only person writing CSS, or you and one other developer have a shared understanding of the codebase, the overhead of a utility-first framework is not justified. You are not solving a coordination problem because there is no coordination problem.
Long-lived projects with stable designs. If the design is settled and changes are infrequent, well-structured vanilla CSS is easier to maintain. You read .pricing-card and know exactly what it is. Reading flex flex-col gap-4 p-6 rounded-xl border border-gray-200 bg-white shadow-sm requires parsing.

The Bundle Size Question
This comes up in every Tailwind discussion, so let us address it directly.
Tailwind’s JIT compiler produces small output. A typical Tailwind project ships 8-15 KB of CSS (gzipped). That is fine. It is not a problem for most projects.
But there is a nuance. Tailwind’s output scales with your markup surface area. A large application with hundreds of components will produce a larger Tailwind stylesheet than a simple site. Vanilla CSS scales with your design system complexity, which is typically smaller.
Here is a real comparison from two Threshline projects:
| Metric | Threshline site (vanilla) | Client dashboard (Tailwind) |
|---|---|---|
| Pages | ~20 | ~45 |
| Components | ~15 | ~80 |
| CSS output (gzipped) | 2.8 KB | 12.4 KB |
| CSS file count | 1 | 1 |
Both are fine. Neither is a performance problem. But if you are optimizing for the absolute smallest payload — and sometimes we are, especially on landing pages — vanilla CSS gives you more control.
The Readability Debate
Tailwind critics say utility classes are unreadable. Tailwind advocates say CSS files are hard to navigate. Both are right, depending on what you are used to.
Here is our honest assessment:
Tailwind is scannable. You can look at a component and immediately see its layout, spacing, and colors. You do not need to check a separate file. This is genuinely useful during code review and debugging.
Tailwind gets noisy at scale. A component with 15-20 utility classes per element becomes hard to parse. The @apply directive helps but defeats part of the purpose. Extracting components helps more.
<!-- This is getting hard to read -->
<button class="inline-flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-blue-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:opacity-50 disabled:cursor-not-allowed">
Submit
</button>
/* Vanilla equivalent — longer to set up but easier to read and reuse */
.btn-primary {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.625rem 1rem;
font-size: 0.875rem;
font-weight: 600;
color: white;
background-color: var(--color-primary);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
transition: background-color 0.15s ease;
&:hover { background-color: var(--color-primary-dark); }
&:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
&:disabled { opacity: 0.5; cursor: not-allowed; }
}
The vanilla version is more lines of code but each line communicates one thing. The Tailwind version is one line of code that communicates twenty things. Neither is objectively better — it depends on what your team optimizes for.
The Hybrid Approach
We increasingly use a hybrid model: Tailwind for layout and spacing utilities, custom CSS for complex component styles. Tailwind’s @layer support makes this clean:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.animated-card {
/* Complex styles that don't map well to utilities */
background: linear-gradient(135deg, var(--tw-gradient-from), var(--tw-gradient-to));
clip-path: polygon(0 0, 100% 0, 100% calc(100% - 2rem), 0 100%);
transition: clip-path 0.3s ease, transform 0.3s ease;
&:hover {
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
transform: translateY(-4px);
}
}
}
This gives you the best of both: Tailwind’s spacing and layout utilities where they shine, and custom CSS where you need the full power of the language. Most of our Tailwind projects end up here eventually.

How We Decide
Here is the actual decision tree we use at Threshline:
- Is this a content/marketing site with a stable design? Vanilla CSS.
- Is this an application UI with many components? Tailwind.
- Is the team familiar with Tailwind? If not, and the timeline is tight, use what they know.
- Is this an MVP that will be rewritten? Tailwind, for speed.
- Are we inheriting an existing codebase? Match what is there. Mixing approaches is worse than either one alone.
The answer is almost never ideological. It is practical. Both tools produce good CSS. The question is which one produces less friction for this team, on this project, right now.
Our Setup for Each
When we go vanilla, our CSS architecture looks like this:
styles/
tokens.css /* Custom properties: colors, spacing, type */
reset.css /* Minimal reset */
global.css /* Base typography, layout primitives */
components/ /* Per-component styles */
When we go Tailwind, our setup is:
tailwind.config.ts /* Extended theme, custom plugins */
styles/
app.css /* Tailwind directives + custom @layer styles */
Both are simple. Both scale. The key is picking one approach and committing to it for the duration of the project. The worst CSS codebases we have seen are the ones that could not decide.
The Honest Truth
If you are a strong CSS developer who enjoys writing well-structured stylesheets, vanilla CSS with modern features is excellent. You will ship smaller bundles, have complete control, and enjoy the process.
If you are building applications with a team, shipping fast, and iterating constantly, Tailwind removes an entire category of decisions and keeps your styles co-located with your components. That has real value.
We use both. We will keep using both. The industry’s tendency to pick sides on this is not helpful. Pick the tool that matches your context and ship something.
If you are building a project and not sure which approach fits, reach out at [email protected]. We are happy to talk it through.