Styling Approaches
Every frontend application has to answer one question early: how do you write and organize CSS as the codebase and the team grow. The options range from plain stylesheets to utility classes to writing styles in JavaScript. They are not interchangeable. Each one makes a different trade between how styles are scoped, how pleasant they are to write, how much work happens at runtime, and how well they fit a modern React app that renders on the server.
This article compares the main approaches and gives you a way to choose between them.
What the choice is really about
Before looking at any tool, it helps to know the factors you are comparing on. Most styling decisions come down to these:
- Scoping. Plain CSS is global. A class you write in one file applies everywhere, so two unrelated components can collide. Every modern approach exists mainly to solve this.
- Authoring and colocation. How close the styles live to the component, and how familiar the syntax feels.
- Dynamic styling. How easily a style can depend on a JavaScript value, such as a prop or a piece of state.
- Runtime cost. Whether styling work happens in the browser while the page renders, or ahead of time during the build.
- Server rendering and React Server Components. Whether the approach works without extra setup when the page renders on the server.
- Design tokens and theming. How the approach supports a shared scale of colors, spacing, and type.
Keep these in mind as you read. The right choice depends on which of them your project cares about most.
Plain CSS
Plain CSS means writing stylesheets by hand and linking them, with no library in between.
Modern CSS now covers much of what used to require tooling. Custom properties (CSS variables) replace many uses of preprocessor variables. Nesting, :has(), container queries, and cascade layers (@layer) handle patterns that once needed Sass or a framework. So plain CSS is more capable than it was a few years ago.
Pros
- No tooling and no runtime cost. The browser reads the CSS directly.
- Nothing new to learn, and full access to every native CSS feature.
Cons
- Styles are global and unscoped. Naming conventions like BEM reduce collisions but take constant discipline and still fail at scale.
- Sharing data between CSS and JavaScript is awkward. The usual bridge is to set a CSS variable from JavaScript.
A build tool such as PostCSS or Lightning CSS can add autoprefixing and transpile newer syntax, which removes the old chore of writing vendor prefixes by hand.
CSS Modules
CSS Modules let you write ordinary CSS in a file and import it into a component. The build step rewrites every class name to something unique, so the styles are scoped to that file automatically.
/* button.module.css */.button { padding: 8px 16px; border-radius: 8px;}import styles from './button.module.css';function Button() { return <button className={styles.button}>Save</button>;}Pros
- Solves scoping without changing how you write CSS. The same
.buttonname in another file will not collide. - Familiar syntax and almost no learning curve.
- No runtime cost, and it works on the server, so it fits React Server Components.
Cons
- No dynamic styling from JavaScript values on its own. You bridge to JavaScript through CSS variables.
- Global styles need a special
:globalsyntax that is not part of the CSS specification. - With TypeScript you need typed module declarations or a plugin so the import is recognized.
CSS Modules are often paired with PostCSS or Sass for autoprefixing and other conveniences.
Utility-first: Tailwind
Tailwind gives you a large set of small utility classes (flex, p-4, text-lg) that you compose directly in the markup, rather than writing separate rules.
<button className="px-4 py-2 rounded-lg bg-blue-600 text-white"> Save</button>Pros
- Scoping stops being a problem, because the classes are fixed and shared rather than named per component.
- The utilities are built from a design scale (spacing, sizes, colors), which encourages a consistent design system instead of arbitrary pixel values.
- It is not tied to React, and it produces static CSS, so it works with server rendering and React Server Components.
- Version 4 (2025) added a faster build engine and lets you configure the theme in CSS instead of a JavaScript config file.
Cons
- There is a learning curve while you memorize the utility names.
- Markup gets long, since a single element can carry many classes, which some people find hard to read.
- Not everything maps to a utility, so you still need a way to write occasional plain CSS.
CSS-in-JS
CSS-in-JS means writing styles in JavaScript or TypeScript, usually next to the component. It splits into two families that behave very differently.
Runtime CSS-in-JS
Libraries like styled-components and Emotion turn styles into components and inject them into the page as it renders.
const Button = styled.button` padding: 8px 16px; background: ${(props) => props.$primary ? 'blue' : 'gray'};`;Pros
- Scoping is solved, and styles live with the component.
- Dynamic styling is natural, because a style can read props and state directly.
- The authoring experience is good, with support for theming and animations.
Cons
- Work happens in the browser at render time, which has a cost.
- It fits poorly with React Server Components. These libraries rely on React Context and inject styles at runtime, so they need client component boundaries or extra server-rendering setup.
This last point has changed what teams choose. styled-components entered maintenance mode in 2025, and its maintainer recommends against adopting it for new projects. Runtime CSS-in-JS is still fine in an existing client-rendered app, but it is no longer the default for something new.
Zero-runtime CSS-in-JS
This newer family keeps the experience of writing styles in JavaScript but compiles them to static CSS during the build, so nothing runs in the browser. Examples are vanilla-extract, Panda CSS, and StyleX (open sourced by Meta).
Pros
- The same colocation and type safety, with no runtime cost.
- Produces static CSS, so it works with server rendering and React Server Components.
- Strong support for design tokens and theming through CSS variables.
Cons
- A build step is required.
- Fully dynamic, runtime-driven values still go through CSS variables rather than arbitrary JavaScript, so it is slightly less flexible than the runtime libraries.
- The ecosystems are newer and smaller than Tailwind or CSS Modules.
Zero-runtime suits teams that want the ergonomics of CSS-in-JS without the runtime and server-rendering problems of the older libraries.
A quick comparison
| Approach | Scoping | Runtime cost | Server Components | Dynamic from JS |
|---|---|---|---|---|
| Plain CSS | Global | None | Yes | Via CSS variables |
| CSS Modules | Automatic | None | Yes | Via CSS variables |
| Tailwind | Not an issue | None | Yes | Via class toggling |
| Runtime CSS-in-JS | Automatic | In the browser | Needs setup | Direct from props |
| Zero-runtime CSS-in-JS | Automatic | None | Yes | Via CSS variables |
How to choose
There is no single best tool, but there are sensible defaults:
- A new app with React Server Components: Tailwind or CSS Modules. Both produce static CSS and work on the server with no extra setup.
- You want to author styles in TypeScript with type safety: a zero-runtime library such as vanilla-extract, Panda CSS, or StyleX.
- An existing client-rendered app already on styled-components or Emotion: keep it, but avoid starting new projects on runtime CSS-in-JS.
- A small or static site: plain CSS or CSS Modules is enough.
The trap to avoid
Adding runtime CSS-in-JS out of habit on a new server-rendered app. It works, but it pushes parts of your tree into client components and adds render-time cost. On a modern React stack, a static-CSS approach is the safer default.
In an interview
An interviewer is not checking whether you can name a library. They want to see that you can justify a choice. Tie your answer to the factors above: how the approach scopes styles, whether it costs anything at runtime, whether it works with server rendering and Server Components, how it handles design tokens, and what the team already knows.