Back to all posts

Styling React in 2026: How I Actually Choose

Jul 07, 2026
13 min read

Ask ten React developers how to style a component and you'll start a fight.

Not a discussion. A fight. Tailwind people and CSS-in-JS people don't disagree about syntax — they disagree about identity. Somewhere along the way, "how do we apply styles" turned into a values debate, and the actual engineering question got buried under it.

Here's the thing that bugs me about that debate: almost none of it is about the question that matters. It's about which approach looks cleanest in a ten-line demo. But nobody styles a ten-line demo for a living. You style an application that grows, gets handed to new people, adds a design system halfway through, and has to survive a framework upgrade.

So the question I actually ask isn't "which one is prettiest." It's:

When this codebase triples in size and I wasn't the one who wrote half of it, which styling approach still makes sense?

That reframes everything. Let me walk through the real options the way I weigh them — Tailwind, CSS Modules, CSS-in-JS, and the zero-runtime newcomers — and where each one genuinely fits.

First, The Question Under The Question

Every styling approach is really answering three separate questions, and people conflate them:

  1. Where does the style live? Same file as the component, a co-located file, or a global sheet.
  2. When is the style computed? At build time, or at runtime in the browser.
  3. How do you stay consistent? Ad-hoc values, or tokens and constraints.

Most "Tailwind vs styled-components" arguments are really arguments about question 2 without anyone saying so. Runtime cost is the thing that quietly decides a lot of this in 2026, especially with React Server Components in the picture. Keep those three questions in mind — they'll do more for you than any hot take.

Tailwind: The Default I Keep Coming Back To

I'll be honest about my bias up front: Tailwind is my default. Not because it's fashionable, and not without reservations. Because of what it does to a team, not to a single component.

function PolicyCard({ policy }: { policy: Policy }) { return ( <article className="rounded-lg border border-slate-200 p-4 hover:border-slate-300"> <h3 className="text-sm font-semibold text-slate-900">{policy.name}</h3> <p className="mt-1 text-xs text-slate-500">{policy.status}</p> </article> ); }

The first reaction most people have is "that's ugly, the markup is noisy." Fair. I had the same reaction. Then I worked in codebases where Tailwind had been the standard for a year, and the noise stopped mattering. Here's what I noticed instead:

  • Nobody invents new spacing values. p-4 is p-4 everywhere. The design constraints are baked into the class names, so drift is hard.
  • Deleting a component deletes its styles. No orphaned CSS rotting in a global file because everyone's afraid to remove it.
  • No naming tax. I don't spend energy inventing .policy-card__status--pending. The cognitive cost of good CSS class names is real, and Tailwind just removes it.
  • It compiles away. The output is a static stylesheet. Zero runtime style computation, which means it plays nicely with Server Components.

The trade-offs are real too, and I won't pretend otherwise. The markup is genuinely harder to scan at first. Long class strings need something like clsx or tailwind-merge once conditions appear. And onboarding someone who's never used it costs a day or two of "why is everything a class name."

The reservation I take most seriously: Tailwind is weak when a style is genuinely dynamic — driven by a value only known at runtime, like a chart bar's height from data. You don't want h-[347px] generated on the fly. For that, an inline style or a CSS variable is the right tool, and mixing them in is fine. Tailwind for the static 95%, a CSS variable for the dynamic 5%.

That's my honest position: Tailwind wins on team-scale maintainability, and that's the axis I care about most. But "it wins for me" is not "it wins for everyone," so let me give the alternatives a real hearing.

CSS Modules: The Boring, Correct Answer

If Tailwind's class soup is a dealbreaker for you, CSS Modules is the option I respect the most.

import styles from './PolicyCard.module.css'; function PolicyCard({ policy }: { policy: Policy }) { return ( <article className={styles.card}> <h3 className={styles.title}>{policy.name}</h3> <p className={styles.status}>{policy.status}</p> </article> ); }
/* PolicyCard.module.css */ .card { border-radius: 0.5rem; border: 1px solid var(--color-border); padding: 1rem; }

What I like: it's just CSS. No new mental model, no runtime, locally scoped class names so you can't leak styles across the app. It compiles to a static stylesheet, so like Tailwind it's Server Components-friendly. If a team has strong CSS skills and hates utility classes, this is not a compromise — it's a genuinely solid choice.

What pushes me away from it at scale: it doesn't enforce consistency on its own. Nothing stops one file from using padding: 16px and another using padding: 1rem and a third using padding: 14px because someone eyeballed it. You get scoping for free, but you have to build the discipline — the tokens, the shared variables — yourself. Tailwind ships that discipline in the box. That's the whole difference, and for some teams CSS Modules plus a well-guarded token file is exactly right.

CSS-in-JS: Why I'm Walking Away From It

This is the one where I've changed my mind, so I'll say it plainly.

I used to reach for styled-components and Emotion without a second thought. The developer experience is lovely: styles co-located with the component, full access to props, dynamic styling that feels natural.

const Card = styled.article<{ $pending: boolean }>` border-radius: 0.5rem; padding: 1rem; border: 1px solid ${(p) => (p.$pending ? '#f59e0b' : '#e2e8f0')}; `;

That reads great. So why am I walking away?

Because the cost moved. Runtime CSS-in-JS computes styles in the browser, on render, and serializes them into the DOM as the app runs. For years that cost was acceptable. Then React Server Components arrived, and runtime CSS-in-JS sits awkwardly with them — these libraries lean on runtime context and client-side execution, which is exactly what Server Components are trying to avoid. The React team's own guidance nudged people toward compile-time styling for a reason.

I don't say this to bury a whole category — if you're on a heavily client-rendered app and the DX makes your team faster, runtime CSS-in-JS is not a crime. But for new work in 2026, especially anything touching Server Components, I no longer start here. The direction of the ecosystem is away from runtime style computation, and I'd rather not build on the side that's swimming against it.

Which leads directly to the interesting middle ground.

Zero-Runtime CSS-in-JS: The Best of Both, Mostly

Tools like vanilla-extract keep the part of CSS-in-JS people actually love — writing styles in TypeScript, with type-safe tokens — while throwing away the part that hurts: the runtime.

// PolicyCard.css.ts import { style } from '@vanilla-extract/css'; import { tokens } from '../theme.css'; export const card = style({ borderRadius: tokens.radius.md, border: `1px solid ${tokens.color.border}`, padding: tokens.space.md, });

You author in TypeScript, get autocomplete and type-checking on your design tokens, and it all compiles to a static stylesheet at build time. Zero runtime. Server Components-friendly. Honestly, this is the most intellectually satisfying option on the list — it fixes the exact thing that made me leave runtime CSS-in-JS.

So why isn't it my default? Two reasons, and neither is technical. The ecosystem and hiring pool around Tailwind are enormous, and the token-based type safety that vanilla-extract gives you, Tailwind approximates well enough through its config. If your team already thinks in TypeScript tokens and wants that safety enforced by the compiler, vanilla-extract is a genuinely excellent pick — arguably the "correct" one on paper. I just find Tailwind gets a team to the same maintainability with less setup.

The Thread That Ties It Together: Tokens

Notice what quietly showed up in almost every example above — var(--color-border), tokens.space.md, Tailwind's slate-200. That's the part that actually matters, and it's independent of which library you pick.

The styling library is a delivery mechanism. Design tokens are the actual system.

Whatever you choose, the thing that keeps a large app coherent is a single source of truth for color, spacing, radius, and typography — and the discipline to never bypass it with a magic number. Tailwind gives you tokens through its config. Vanilla-extract gives you tokens as typed objects. CSS Modules gives you tokens through CSS custom properties. The delivery differs; the principle doesn't. If you get tokens right, you can survive changing your styling library later. If you get them wrong, no library saves you.

So What Do I Actually Pick?

Stripped of tribalism, here's my honest decision path:

  • New app, normal team, wants to move fast and stay consistent → Tailwind. It ships the constraints in the box.
  • Team with strong CSS culture that dislikes utility classes → CSS Modules plus a guarded token file. Boring and correct.
  • Team that lives in TypeScript and wants compiler-enforced tokens → vanilla-extract. The most "principled" pick.
  • Heavily client-rendered app where runtime DX wins → runtime CSS-in-JS is still defensible, eyes open about Server Components.

There's no universally right answer, and anyone who tells you there is hasn't worked in enough different codebases. For this series I'll use Tailwind, because it fits the kind of app we're building and it pairs cleanly with what comes next.

Because styling rarely lives alone. The moment you have a real app, you reach for a component library — and how it's styled decides whether you can theme it, override it, or fight it. That's the next article: choosing a React UI library, and why the styling decision you just made quietly constrains that choice.

If your team is mid-argument about this right now, try changing the question. Stop asking which one is best and start asking which one your codebase can live with in two years. And if you've landed somewhere I'd disagree with — especially if you're still happily on runtime CSS-in-JS — I genuinely want to hear the case. I've changed my mind on this before.

Found this helpful?

Let's discuss your project needs.

Get in touch