Few topics generate more bad takes than "Tailwind vs CSS-in-JS." The argument usually devolves into aesthetic preferences and tweet-length objections. Let's do better. This is a pragmatic look at the three approaches teams actually choose between in 2026, what they're good at, and how to pick between them.
The contestants
We're really comparing three options:
- Utility-first CSS (Tailwind) — write classes like
flex items-center px-4directly in your markup. - CSS Modules — write regular CSS, scoped per file, imported as objects.
- CSS-in-JS — write styles as JavaScript, generally with a library like Vanilla Extract, Linaria, or styled-components.
Each one solves a real problem. Each one creates new ones.
What everyone is actually arguing about
Strip away the rhetoric and the debate is about three trade-offs:
- Where does style live? Next to the component, in a separate file, or compiled from JavaScript?
- How dynamic is your styling? Mostly static design tokens, or values computed at runtime from props?
- What's your team's tolerance for build complexity? Tailwind has its own complexity. So does CSS-in-JS. There's no zero-cost option.
Once you frame it that way, the choice usually becomes obvious.
When Tailwind wins
Tailwind is at its best when:
- You have a stable design system you can encode as utility classes.
- Your team has more than one person and you want them to write consistent CSS without effort.
- You care about runtime performance and bundle size.
- You'd rather read markup than jump between files to understand styles.
The complaint that "Tailwind is just inline styles" misses the point. Inline styles can't do hover states, media queries, dark mode, or design tokens. Tailwind can. The constraint is the feature: you can't write padding: 13px; because design systems shouldn't have 13px paddings.
Two things Tailwind genuinely makes harder:
- Highly dynamic styles computed from props. You can do it (
cn(...)helpers,clsx, conditional class strings), but it's never as clean as a CSS-in-JS template. - Themes that aren't simple variations. Tailwind's theming is good for swapping a color palette. It's awkward for fundamentally different layouts per theme.
When CSS Modules win
CSS Modules are the boring choice that often wins on long-running projects.
They're at their best when:
- You have a designer or an HTML-first developer on the team who's fluent in CSS.
- Your styling vocabulary doesn't fit cleanly into utility classes (animations, complex
:nth-childselectors, intricate grid layouts). - You want zero runtime overhead and a build setup that hasn't changed in five years.
The honest downside: nothing prevents inconsistency. Two engineers will inevitably write two different "spacing-medium" classes if you don't agree on shared variables. CSS Modules need a culture of design tokens to stay clean.
If you go this route, set up a small file of CSS variables or a SCSS theme module and treat it as the single source of design truth. The discipline beats the tooling here.
When CSS-in-JS wins
CSS-in-JS used to be the default for React apps. In 2026, it's a more specialized choice. It's at its best when:
- Your styles genuinely depend on props or component state in non-trivial ways.
- You want types on your styles (Vanilla Extract is excellent for this).
- You're building a library of components that need to be themed by consumers.
The catch is performance. Runtime CSS-in-JS libraries (styled-components, Emotion in classic mode) have measurable overhead, and they don't play well with Server Components. Modern CSS-in-JS that compiles at build time (Vanilla Extract, Linaria, Panda CSS) sidesteps that, but at the cost of more build complexity.
If you're starting a new project in 2026 and reaching for runtime CSS-in-JS, ask yourself why. Most of the original motivations have been solved by other means.
The practical comparison
Let's compare a single component across the three approaches. Tailwind:
export function Button({ variant }: { variant: "primary" | "ghost" }) {
return (
<button
className={
variant === "primary"
? "rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
: "rounded-md px-4 py-2 text-blue-600 hover:bg-blue-50"
}
>
Click me
</button>
);
}
CSS Modules:
import styles from "./Button.module.css";
export function Button({ variant }: { variant: "primary" | "ghost" }) {
return (
<button className={styles[variant]}>Click me</button>
);
}
/* Button.module.css */
.primary {
border-radius: 0.375rem;
padding: 0.5rem 1rem;
background: var(--color-blue-600);
color: white;
}
.primary:hover {
background: var(--color-blue-700);
}
.ghost {
/* ... */
}
CSS-in-JS (Vanilla Extract):
import { style } from "@vanilla-extract/css";
const primary = style({
borderRadius: "0.375rem",
padding: "0.5rem 1rem",
background: "var(--color-blue-600)",
color: "white",
":hover": { background: "var(--color-blue-700)" },
});
None of these is dramatically shorter than the others. The difference is where the cost lands:
- Tailwind: cost is in the markup.
- CSS Modules: cost is the round trip between files.
- CSS-in-JS: cost is build complexity.
Pick the one whose cost bothers you least.
Things that don't matter as much as you think
A few common arguments that usually aren't load-bearing:
- "My JSX is unreadable with Tailwind." Editors can wrap and sort classes. The "wall of text" complaint dissolves with a single Prettier plugin.
- "CSS-in-JS is slow." True for runtime libraries, mostly false for compile-time ones. Pick the right kind.
- "I prefer plain CSS." That's fine, but on a four-engineer team your preference doesn't determine consistency. Tooling does.
A reasonable default in 2026
If we were starting a new web app today and didn't have a strong reason to do otherwise, we'd reach for Tailwind plus a small set of CSS files for global styles, animations, and the rare custom layout. That gets us:
- Tailwind for ~95% of styling.
- A
globals.cssfor resets and base styles. - A few CSS variables for design tokens.
- No runtime CSS-in-JS. Server Components stay simple, bundle stays small.
If you're building a component library meant to be themed by consumers, that calculus shifts. Vanilla Extract or Panda CSS becomes more attractive.
If you have a CSS-fluent team and a long-lived codebase, CSS Modules are still an excellent, low-drama choice.
How to actually decide
Run this short checklist:
- Will the same two or three engineers maintain this for years? CSS Modules are fine.
- Are you a larger team that needs strong consistency by default? Tailwind.
- Are your styles genuinely dynamic in non-trivial ways? Compile-time CSS-in-JS.
- Are you using Server Components? Prefer Tailwind or compile-time CSS-in-JS over runtime libraries.
There's no universally right answer. There's a right answer for your team, your app, and the next two years of your roadmap. Pick the one whose trade-offs you can live with, then commit. Switching styling systems mid-project is the worst outcome of all.