ReactStyling Approaches Overview

Styling Approaches Overview

React does not ship with a built-in styling system. That is intentional — it lets teams choose the approach that best fits their project. The result is a rich ecosystem of options, each with distinct trade-offs around performance, developer experience, SSR compatibility, and bundle size. Understanding those trade-offs is what separates junior React developers from senior ones.

There are five mainstream approaches used in production React applications today: Global CSS, CSS Modules, CSS-in-JS (Styled Components, Emotion), Utility-first CSS (Tailwind), and Inline styles. This page maps out all five so you can make informed decisions.

1. Global CSS

The simplest approach: write .css files and import them. Styles are globally scoped — any rule you write applies to every element on the page that matches the selector.

CSS
/* styles/global.css */
.button {
  background: #0070f3;
  color: white;
  padding: 8px 16px;
  border-radius: 4px;
}

.button:hover {
  background: #0051a2;
}

JSX
import './styles/global.css'

function Button({ children }) {
  return <button className="button">{children}</button>
}

Global CSS works fine for small apps or when you deliberately want site-wide rules (resets, typography scales, CSS custom properties). It breaks down in larger codebases: class names collide, specificity wars break out, and deleting a CSS rule becomes dangerous because you never know what depends on it.

2. CSS Modules

CSS Modules give each .module.css file a local scope by transforming class names at build time. The class .button in Button.module.css becomes something like Button_button__xK2qP in the final output, making collisions impossible.

CSS
/* Button.module.css */
.button {
  background: #0070f3;
  color: white;
  padding: 8px 16px;
}

JSX
import styles from './Button.module.css'

function Button({ children }) {
  return <button className={styles.button}>{children}</button>
}

CSS Modules are supported out of the box in Vite, Create React App, and Next.js. They require zero runtime, work perfectly with SSR, and give you full access to the CSS language. The limitation is that dynamic styles (e.g., a color that changes based on a prop) are awkward — you end up using CSS custom properties or toggling class names.

3. CSS-in-JS (Styled Components & Emotion)

CSS-in-JS libraries let you write CSS directly inside JavaScript using tagged template literals. The component and its styles live in the same file, and dynamic styling based on props is first-class.

JSX
import styled from 'styled-components'

const Button = styled.button`
  background: ${(props) => (props.primary ? '#0070f3' : 'white')};
  color: ${(props) => (props.primary ? 'white' : '#0070f3')};
  padding: 8px 16px;
  border-radius: 4px;
`

// Usage:
<Button primary>Save</Button>
<Button>Cancel</Button>

The developer experience is excellent — colocation of styles and logic, automatic vendor prefixing, theming via ThemeProvider. The cost is a JavaScript runtime: styles are injected via <style> tags at runtime, which adds CPU overhead and can cause flashes during SSR hydration. This is why the industry has been moving away from runtime CSS-in-JS since ~2022.

4. Utility-First CSS (Tailwind)

Tailwind CSS provides thousands of atomic utility classes — flex, pt-4, text-blue-500, rounded-lg — that you compose directly in JSX. There is no separate CSS file to maintain; the styles live in the className prop.

JSX
function Button({ children, primary }) {
  return (
    <button
      className={
        primary
          ? 'bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700'
          : 'border border-blue-600 text-blue-600 px-4 py-2 rounded hover:bg-blue-50'
      }
    >
      {children}
    </button>
  )
}

Tailwind's build step strips unused classes, so production bundles are tiny (typically 5–15 KB). It dominates the React ecosystem in 2024–2025, powering major component libraries like Shadcn/ui, Radix UI themes, and Headless UI. The trade-off is a steep initial learning curve and verbose className strings for complex components.

5. Inline Styles

React's style prop accepts a JavaScript object with camelCase property names. No build step, no class names, purely dynamic — but no access to pseudo-selectors, media queries, or animations.

JSX
function ProgressBar({ value }) {
  return (
    <div style={{ background: '#e5e7eb', height: 8, borderRadius: 4 }}>
      <div
        style={{
          width: `${value}%`,
          background: '#0070f3',
          height: '100%',
          borderRadius: 4,
          transition: 'width 0.3s ease',
        }}
      />
    </div>
  )
}

Inline styles are best reserved for truly dynamic computed values — things like animation percentages, chart dimensions, or user-chosen colors — where you need JavaScript-computed values that cannot be expressed as static CSS class names.

Comparison Table

Approach

Scoping

Dynamic styling

SSR support

Runtime cost

DX

Bundle size impact

Global CSS

None (global)

Manual class toggling

Excellent

None

Simple but fragile

Small

CSS Modules

Auto per-file

CSS custom props / class toggle

Excellent

None

Great

Small

Styled Components

Automatic

First-class (props)

Complex (SSR setup)

Medium–High

Excellent

Medium (+runtime)

Emotion

Automatic

First-class (props)

Complex

Medium

Excellent

Medium (+runtime)

Tailwind

Atomic / global

Class composition

Excellent

None (build-time)

Great (after ramp-up)

Tiny (purged)

Inline styles

Element-level

Native (JS object)

Excellent

None

Limited (no pseudo)

None

When to Pick Each
  • Global CSS — site-wide resets, design tokens as CSS custom properties, or tiny prototypes

  • CSS Modules — component libraries, open-source packages, teams that want zero runtime and full CSS power

  • Styled Components / Emotion — existing codebases built on them; avoid for new projects

  • Tailwind — the default choice for most new React/Next.js apps in 2024–2025; excellent for design-system-free projects

  • Inline styles — dynamic computed values (chart widths, user-picked colors, animated numeric properties)

The Evolution of React Styling

The history of React styling is a story of progressively eliminating global scope. In 2015 everyone used global CSS with BEM naming conventions. In 2016–2018 CSS Modules gained traction, followed by an explosion of CSS-in-JS solutions (Styled Components 2016, Emotion 2017). By 2020 Tailwind started taking over new projects. By 2023 the React team itself began discouraging runtime CSS-in-JS in the Server Components era, as injecting <style> tags at runtime is fundamentally incompatible with server-first rendering.

Current industry direction (2025): Tailwind for application UIs, CSS Modules for library authors who need zero runtime and maximum compatibility, and CSS-in-JS only in legacy codebases. Both styled-components and emotion are in maintenance mode for their runtime variants and are actively developing compile-time / static extraction modes to remove the runtime cost.

Note
You can mix approaches in the same project. Many teams use Tailwind for layout and spacing, CSS Modules for complex component styles that need pseudo-selectors, and CSS custom properties (defined globally) for theming.
Tip
For a new Vite + React + TypeScript project in 2025, start with Tailwind CSS and the `clsx` or `cn` utility for conditional class composition. It scales well and aligns with the component libraries you will likely adopt.