Tailwind vs CSS-in-JS vs plain CSS: when to use which

Status: đźź© COMPLETE Last updated: 2026-06-21 Plain-English tagline: Three approaches to styling React/Next.js apps. They produce identical visuals; the difference is in developer experience, performance, and how you reason about styles. For new projects in 2026, Tailwind wins by default.


What this decides

When building a React/Next.js component, where do the styles live?

  • Tailwind — utility classes (bg-blue-500 p-4 hover:bg-blue-600) directly in JSX
  • CSS-in-JS — styles written in JavaScript objects or template strings (styled-components, Emotion, Stitches, etc.)
  • Plain CSS — separate .css files with traditional selectors, imported into components
  • CSS Modules — .module.css files that scope class names per file

For background: Tailwind CSS 🟩 🟦, CSS 🟩, ui 🟩 🟦.


The short answer

For new React/Next.js projects in 2026: Tailwind + shadcn/ui. This is the modern default and what the encyclopedia’s playbook recommends.

Use CSS Modules when Tailwind feels wrong for the use case (heavy custom styling, design-system-driven brand work).

Use plain CSS when you’re styling truly global concerns (typography defaults, CSS variables, :root resets).

Avoid runtime CSS-in-JS (styled-components, Emotion’s css prop) — once-dominant, now declining due to performance overhead in React Server Components and harder SSR. The community has shifted away.


The factors that matter

  1. Server vs client rendering. Runtime CSS-in-JS (extracting styles at render time) is incompatible with React Server Components — they don’t have a client runtime to insert <style> tags into. Tailwind sidesteps this entirely (CSS is generated at build time).
  2. Speed of development. Tailwind eliminates the “name this class” decision and the context-switch between JSX and a separate CSS file.
  3. Bundle size. Tailwind generates only the utilities you use. Runtime CSS-in-JS ships a CSS engine to the browser; even at small scale that’s ~20–40KB.
  4. Design system consistency. Tailwind’s design tokens (spacing scale, color palette) enforce consistency by default. Custom CSS lets each developer pick “almost the same blue” 17 times.
  5. Team familiarity. Tailwind has a learning curve (memorize utility names). Plain CSS is universal.
  6. Customization. Tailwind is excellent for compose-existing-tokens; trickier for “this one element needs a unique shadow pattern” — though style={{}} always works as an escape hatch.

When to pick TAILWIND

  • Most new projects in 2026. Default.
  • Anywhere you want speed of iteration. Style as you build; no context-switch.
  • Anywhere you use shadcn/ui. shadcn is built on Tailwind; mixing in CSS-in-JS adds friction.
  • For consistent spacing/colors. The design tokens are first-class.
  • For mobile-first responsive design. The breakpoint prefixes (sm:, md:, lg:) make responsive trivially obvious.
  • For dark mode. The dark: prefix is built-in.

Bible Quest uses Tailwind everywhere except globals.css (for design tokens, CSS variables, and theme switching). Every component in src/components/ui/ and beyond is Tailwind.


When to pick CSS MODULES

  • You need complex, deeply-nested selectors that Tailwind utilities can’t express cleanly.
  • You’re styling a third-party component’s internals via descendant selectors.
  • You have a strong design system already built in CSS and porting it to Tailwind would be wasted effort.
  • You want scoped CSS without learning Tailwind’s syntax.

Example use in Next.js:

// Card.tsx
import styles from "./Card.module.css";
 
export function Card({ children }) {
  return <div className={styles.card}>{children}</div>;
}
/* Card.module.css */
.card {
  background: white;
  border-radius: 8px;
  /* unique cross-cutting selectors that Tailwind would be awkward for */
}
.card :global(.legacy-component) {
  /* style a third-party child */
}

CSS Modules work in Next.js out of the box (any .module.css file).


When to pick PLAIN CSS (globals.css)

  • Global styles — body/html resets, font setup, CSS variables.
  • Print stylesheets.
  • Theme tokens that you’ll reference from Tailwind (--background, --primary) — shadcn’s standard pattern.
  • Custom CSS for things Tailwind can’t do — keyframe animations with named identifiers, complex @supports queries, container queries with named contexts.

Bible Quest’s globals.css defines the dark-mode color flips and the bg-page gradient utility — about 80 lines. Everything else is Tailwind.


When to AVOID runtime CSS-in-JS (styled-components, Emotion)

These were dominant 2018–2022. In 2026 they’re declining because:

  1. Incompatible with React Server Components — they need a client runtime to inject styles, but Server Components don’t have one.
  2. Bundle cost — the runtime ships ~20–40KB of CSS engine to every page.
  3. SSR complexity — getting hydration right requires careful library-specific setup.
  4. Build-time CSS-in-JS alternatives exist — Vanilla Extract, Linaria, Panda CSS — that solve the problems but they’re niche.

If you’re already on styled-components: it still works. The migration cost to Tailwind for a large codebase is real. Stay put unless you’re hitting the RSC limitations.

For new projects: skip it. Tailwind covers the use cases without the cost.


Hybrid is normal

A typical Next.js app uses multiple approaches:

  • globals.css — design tokens, body defaults, the dark class behavior
  • Tailwind utilities — 95% of component styling
  • Inline style={{}} — rare one-off computed styles (style={{ '--progress': ${pct}% }})
  • CSS variables — for runtime theming where Tailwind config wouldn’t reach

This is fine. No project should be 100% one approach.


What if I’ve already chosen?

“I’m on styled-components and want to migrate to Tailwind”: big project. Migrate gradually: install Tailwind alongside; new components use Tailwind; existing components migrate when touched. Expect 6+ months for a full conversion on a large codebase.

“I started with plain CSS and want to add Tailwind”: install Tailwind, configure content to scan your files. You can use both side-by-side from day one. Migrate at your pace.

“I started with Tailwind and find it ugly in JSX”: the verbose class strings are real. Solutions: extract components for reused patterns (<Button>), use the tailwind-merge library to dedupe at runtime, or just accept it — most developers report the friction fades after a week.

“My team wants CSS Modules; I want Tailwind”: use both. CSS Modules for the components someone wrote that way; Tailwind for new work. Coexist.


See also


Sources