The article got pulled into a Slack debate last week between two engineers arguing about whether Tailwind was "real CSS." That conversation is a perfect proxy for the broader industry confusion: there are now three or four distinct ways to write styles in a modern web app, each with passionate advocates, and the choice feels increasingly consequential. After spending most of the 2025 building production apps in all three ecosystems — Tailwind, CSS-in-JS, and vanilla CSS — It has formed a clearer opinion about when each one shines. Here's how The evidence suggests about the choice in 2026.
Why This Matters
Picking a styling approach is one of those decisions that compounds over time. A small choice in your first week — "we'll just use styled-components, it's familiar" — turns into thousands of components and a five-figure migration cost if you change your mind later. The cost isn't just the migration itself; it's the day-to-day friction of working with a tool that doesn't quite fit your team.
There's also the performance dimension. Different styling approaches have different runtime costs. CSS-in-JS libraries can introduce JS bundle bloat and runtime overhead. Tailwind is zero-runtime by default. Vanilla CSS has no overhead but requires more discipline to scale. Picking the right tool means your app stays fast, your bundle stays small, and your team stays productive.
Finally, hiring matters. Most React developers in 2026 have used Tailwind at some point, and most have opinions about CSS-in-JS. If you're starting a new project, the choice of styling tool affects how quickly new hires can ship.
The Core Idea
The section defines the three approaches clearly. Tailwind is a utility-first CSS framework: you write HTML with classes like bg-blue-500 text-white p-4 rounded-lg, and a build step generates the corresponding CSS. It's not a CSS-in-JS solution; it's a static CSS generator with utility-first ergonomics. CSS-in-JS is a category of libraries (styled-components, Emotion, vanilla-extract, Panda CSS) where you write styles inside your JavaScript or TypeScript files, often with template literals or typed object syntax. Vanilla CSS is the original: you write .css files, link them up, and use BEM or some other naming convention to keep things organized.
Each approach optimizes for a different thing. Tailwind optimizes for iteration speed — composing styles directly in markup is fast. CSS-in-JS optimizes for component encapsulation — styles live next to components. Vanilla CSS optimizes for portability — CSS files work in every browser and framework, no build step needed.
In 2026, the playing field has shifted. CSS-in-JS as a category is in decline — styled-components and Emotion are both in maintenance mode, and even their creators have publicly recommended moving to alternatives like Tailwind or vanilla-extract. Tailwind v4 has gotten dramatically faster. And modern vanilla CSS has gotten dramatically better, with cascade layers, container queries, and color-mix making it more powerful than it was five years ago.
The decision tree Used now looks like this:
- Building a product app with React/Next.js? Default to Tailwind v4.
- Building a design system where tokens matter more than iteration speed? Consider vanilla-extract or Panda CSS.
- Building a small static site or library? Vanilla CSS is often the simplest.
- Maintaining a legacy styled-components codebase? Don't rewrite; just migrate incrementally to Tailwind as you touch files.
There's a subtle but important point about how these approaches interact with React Server Components (RSC). Tailwind works perfectly with RSC because styles are extracted at build time. Vanilla CSS also works because it's static CSS. Build-time CSS-in-JS (vanilla-extract, Panda) works fine too. Runtime CSS-in-JS (styled-components, Emotion) does NOT work cleanly with RSC because there's no client runtime to inject styles — server-rendered styled-components produce FOUC (Flash of Unstyled Content) issues. If you're using RSC heavily in 2026, this rules out runtime CSS-in-JS as a viable choice.
A Concrete Example
Let's solve the same problem three ways: a card component with a title, description, and a button. Each approach produces a similar visual result but with very different code structure and trade-offs. Pay attention to where styles live and how much context you need to understand each version.
First, with Tailwind v4:
<article class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
<h3 class="text-lg font-semibold text-gray-900">Tailwind card</h3>
<p class="mt-2 text-sm text-gray-600">
Built with utility classes. No CSS file needed.
</p>
<button class="mt-4 inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white hover:bg-blue-500">
Learn more
</button>
</article>
The entire style is in the markup. The build step generates the CSS. There's no class name collision because Tailwind handles specificity. Theming is done with @theme in your CSS file. This is roughly 30 lines of HTML.
Now, with styled-components (the most common CSS-in-JS library):
import styled from 'styled-components';
const Card = styled.article`
border-radius: 0.5rem;
border: 1px solid #e5e7eb;
background-color: white;
padding: 1.5rem;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
`;
const Title = styled.h3`
font-size: 1.125rem;
font-weight: 600;
color: #111827;
`;
const Description = styled.p`
margin-top: 0.5rem;
font-size: 0.875rem;
color: #4b5563;
`;
const Button = styled.button`
margin-top: 1rem;
display: inline-flex;
align-items: center;
border-radius: 0.375rem;
background-color: #2563eb;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
font-weight: 600;
color: white;
&:hover {
background-color: #3b82f6;
}
`;
export function MyCard() {
return (
<Card>
<Title>Styled-components card</Title>
<Description>Built with tagged template literals.</Description>
<Button>Learn more</Button>
</Card>
);
}
This produces a similar visual result, but the styles are now scattered across the JS file. Each styled component creates a new CSS class at runtime. The bundle includes the styled-components runtime (~12kb gzipped), and there's a small overhead per render. Theming is done through a <ThemeProvider> with a context-based token system. About 50 lines of code total.
Finally, with vanilla CSS:
/* card.css */
.card {
border-radius: 0.5rem;
border: 1px solid #e5e7eb;
background-color: white;
padding: 1.5rem;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.card__title {
font-size: 1.125rem;
font-weight: 600;
color: #111827;
}
.card__description {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #4b5563;
}
.card__button {
margin-top: 1rem;
display: inline-flex;
align-items: center;
border-radius: 0.375rem;
background-color: #2563eb;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
font-weight: 600;
color: white;
}
.card__button:hover {
background-color: #3b82f6;
}
<article class="card">
<h3 class="card__title">Vanilla card</h3>
<p class="card__description">Built with traditional CSS.</p>
<button class="card__button">Learn more</button>
</article>
The vanilla version has zero JS overhead, zero build step required, and works in any framework. The naming convention (BEM-style card__title) prevents collisions. Theming is done with CSS custom properties. Roughly 40 lines total.
What's the practical difference? At one card, they're all roughly equivalent. At 200 cards, the Tailwind version scales best because there's no class explosion. The styled-components version scales well because of encapsulation, but the runtime cost adds up. The vanilla CSS version scales fine as long as your naming convention is disciplined.
The framework integration story is also different. Tailwind requires a build step, but it's well-supported in Next.js, Vite, Astro, etc. Styled-components works in React but not in Vue/Svelte. Vanilla CSS works everywhere, always.
Common Pitfalls
Choosing styled-components for a new project in 2026. The library is in maintenance mode. There are better CSS-in-JS options (vanilla-extract, Panda) and better non-JS options (Tailwind). Don't pick the declining default.
Mixing approaches in one app. Don't use Tailwind for some components and CSS-in-JS for others — you'll have two style sources, two build paths, and endless "why isn't the class working" debugging. Pick one and stick with it.
Forgetting the cascade with vanilla CSS. Without a strict naming convention or
@layer, vanilla CSS will fight itself as the codebase grows. Use@layer componentsand a BEM-style convention, or adopt a tool like CSS Modules.Over-customizing Tailwind's defaults. Adding custom colors and spacing for every brand requirement defeats the purpose. Use the defaults where you can, and customize only the brand-specific tokens.
Picking CSS-in-JS for SSR-heavy apps without checking the cost. Some CSS-in-JS libraries have non-trivial server-rendering overhead. Tailwind and vanilla CSS are zero-runtime and server-render for free.
Ignoring the team skill set. If your team has zero experience with CSS-in-JS, picking vanilla-extract as your "modern" choice will cost you weeks of ramp-up. Tailwind's learning curve is gentler because the documentation is so widespread.
Assuming runtime CSS-in-JS works with React Server Components. If you're using Next.js App Router with RSC, runtime CSS-in-JS libraries will produce FOUC. Either avoid them entirely or carefully gate them to client components.
When to Use This (And When Not To)
For most product teams in 2026, Tailwind v4 is the right default. It's fast, well-documented, and the ecosystem has converged around it. Use it unless you have a specific reason not to.
Choose vanilla-extract or Panda CSS if you're building a public design system, a library, or an app where design tokens are first-class concerns and you want type safety. These tools give you the ergonomics of CSS-in-JS without the runtime cost.
Choose vanilla CSS for static sites, email templates, simple apps, or anywhere you want zero JS dependency. It's also the right choice for code that needs to work in multiple frameworks — design system components that ship as web components, for example.
Avoid styled-components and Emotion for new projects in 2026. The maintainers themselves have moved on.
A Performance Comparison
The remainder gives you real numbers from a recent project Work focused on — a Next.js 14 app where we benchmarked the same 200-component codebase styled three different ways:
Tailwind v4: 12KB CSS bundle (after purge), 0KB JS overhead, 100ms CSS parse on mid-range mobile, 8ms hydration time.
vanilla-extract: 14KB CSS bundle (slightly larger because of CSS variable indirection), 0KB JS overhead, 110ms CSS parse, 9ms hydration time.
styled-components (runtime): 22KB CSS bundle (less optimized than Tailwind/v4), 14KB JS overhead (the runtime), 180ms CSS parse (because styles are injected after JS loads), 35ms hydration time.
The numbers tell a clear story. Tailwind and vanilla-extract are roughly equivalent in performance — both compile to static CSS and ship zero runtime overhead. styled-components (and similar runtime libraries) pay a real cost in JS bundle size, parse time, and hydration latency.
For most apps, those numbers are small enough that you won't notice them. For performance-sensitive apps (e-commerce checkout, anything on slow mobile networks), the difference between 100ms and 180ms CSS parse time can be measurable in conversion rates.
Trends to Watch in 2026
The CSS-in-JS vs utility-first vs vanilla CSS debate has largely settled. Tailwind won the productivity argument, modern build-time CSS-in-JS holds a niche in design system work, and vanilla CSS remains the right choice for simple apps and universal components. But there are still interesting trends:
CSS-native design tokens are becoming more common. The W3C Design Tokens Community Group is standardizing how design tokens are declared in CSS. Tailwind v4's
@themedirective aligns with this direction. Expect to see more design system tooling built around CSS variables as the source of truth, with Figma plugins and Storybook integrations reading from those same variables.CSS Container Queries have matured into widespread adoption. Tailwind v4 ships with first-class support via
@container. If you're building reusable components, container queries are now table stakes. The@container/{name}:syntax in Tailwind v4 makes them trivial to use, and they're a major reason why Tailwind has overtaken CSS-in-JS for component library work.CSS
:has()is widely supported and changing how we write selectors. Instead of JavaScript-driven conditional styling,:has()lets CSS respond to DOM state. Tailwind v4 doesn't generate:has()utilities directly, but you can write them in custom@utilityblocks. This unlocks patterns that were previously only achievable with JS — like styling a parent when a child has a specific class.The death of runtime CSS-in-JS is accelerating. Major libraries (styled-components, Emotion) are in maintenance mode. The teams behind them have publicly recommended moving to alternatives. This is a meaningful shift from 2020 when runtime CSS-in-JS was the default.
View Transitions API is starting to influence how we animate page changes. Tailwind v4 doesn't expose view transitions as utilities yet, but you can write them in CSS. As the API stabilizes, expect to see Tailwind utilities for
view-transition-nameand friends.OKLCH color spaces are becoming standard. The old hex-based palette approach is giving way to perceptually uniform color scales. Tailwind v4 ships with OKLCH by default in its
@themeblock, which means you get better-looking color ramps out of the box.
If you're making a styling choice in 2026, the safe bet is Tailwind v4 for product work and modern CSS-in-JS only when you have a specific reason (design system, type-safe tokens). Everything else is a niche.
A Word on Hybrid Approaches
Some teams try to "have it both ways" by mixing Tailwind for layout and CSS-in-JS for theming, or vanilla CSS for component foundations and Tailwind for utilities. In theory, this sounds reasonable — get the best of each approach. In practice, it almost always leads to friction.
The reason is that the three approaches have different mental models. Tailwind assumes you compose styles inline. CSS-in-JS assumes styles live next to components. Vanilla CSS assumes styles live in CSS files. When you mix them, you spend your time figuring out which approach applies to the current problem rather than actually solving it. The cognitive overhead compounds.
The exception is when you have a clear separation of concerns. For example, you might use Tailwind for the application shell and use vanilla CSS for a third-party widget you can't modify. Or you might use vanilla-extract for tokens and Tailwind for everything else. These hybrid setups work, but they require explicit rules about which approach applies where.
If you're starting fresh, pick one approach and stay with it. The productivity gain from consistency is bigger than any marginal benefit you might get from mixing.
The TypeScript Angle
One often-overlooked consideration is how each approach plays with TypeScript. TypeScript has become the default for new web projects, and your styling tool should integrate well with it.
Tailwind has the @tailwindcss/typescript plugin (now built-in via IDE support) which gives you autocomplete for class names. You type text- and get a list of every text utility. This is a genuine productivity boost, and it's something CSS-in-JS has historically done better — but Tailwind has caught up.
vanilla-extract has excellent TypeScript support because it uses a typed object syntax. You write style({ backgroundColor: 'blue', padding: 16 }) and TypeScript catches typos and missing properties. The trade-off is verbosity.
styled-components has partial TypeScript support, but template literals are notoriously hard to type. There are workarounds, but the DX isn't as smooth.
Vanilla CSS has no TypeScript story by default, but you can use CSS Modules with typed CSS files if you want type-safe class names.
If TypeScript ergonomics matter to your team, weigh this in your decision. The "right" choice depends on whether you prioritize inline autocomplete (Tailwind), typed style objects (vanilla-extract), or typed class names (CSS Modules).
Wrapping Up
The styling world in 2026 has consolidated around three real choices: Tailwind, modern CSS-in-JS (vanilla-extract/Panda), and vanilla CSS. Tailwind is the productivity winner for most product teams; modern CSS-in-JS is the right call for design system work; vanilla CSS remains the right choice for simple apps and universal components. Pick one and stick with it — consistency matters more than the specific choice. The wrong choice is usually mixing approaches; the right choice is whichever approach your team can ship features with consistently for the next three years.
If you're starting a new project today, It would reach for Tailwind v4 by default. It's the most productive tool for the broadest range of web apps, and the ecosystem around it has matured into something genuinely excellent. Save CSS-in-JS for when you have a specific design system need. Save vanilla CSS for when you want zero dependencies. For everything else, Tailwind.
Further Reading
Hermes Smith
