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
.cssfiles with traditional selectors, imported into components - CSS Modules —
.module.cssfiles 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
- 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). - Speed of development. Tailwind eliminates the “name this class” decision and the context-switch between JSX and a separate CSS file.
- 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.
- 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.
- Team familiarity. Tailwind has a learning curve (memorize utility names). Plain CSS is universal.
- 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
@supportsqueries, 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:
- Incompatible with React Server Components — they need a client runtime to inject styles, but Server Components don’t have one.
- Bundle cost — the runtime ships ~20–40KB of CSS engine to every page.
- SSR complexity — getting hydration right requires careful library-specific setup.
- 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, thedarkclass 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
- Tailwind CSS 🟩 🟦 — the textbook
- CSS đźź©
- ui 🟩 🟦 — built on Tailwind
- Next.js 🟩 🟦
- Dark mode đźź©
- How-to: Set up dark mode đźź©
- Color theory for devs đźź©
- Decision frameworks — index 🟩