Styling Paradigms: CSS-in-JS vs. Traditional Stylesheets

In modern React development, how you style your components is as crucial as how you build them. The right styling strategy can lead to a maintainable, scalable, and enjoyable developer experience, while the wrong one can result in a tangled mess of conflicting rules and “!important” hacks.

This chapter explores the two dominant paradigms for styling React applications:

By the end, you’ll understand the philosophy, pros, and cons of each approach, empowering you to make an informed decision for your next project.


The Traditional Path: Separation with CSS & SCSS

The classic approach to web styling is built on the principle of separation of concerns: HTML provides structure, JavaScript handles behavior, and CSS manages presentation. In a modern React app, this means keeping your styles in dedicated files, separate from your component logic.

SCSS (Sassy CSS) is a popular CSS preprocessor that enhances standard CSS with features like variables, nested rules, and mixins, making stylesheets more organized and powerful.

However, CSS’s global nature presents a challenge. A style defined for a .button in one file can unintentionally affect a .button elsewhere. The modern solution to this is CSS Modules.

How CSS Modules Work

When you import a stylesheet as a module (e.g., import styles from './Button.module.scss'), your build tool transforms the class names into unique, locally-scoped identifiers. This guarantees that styles for one component won’t leak out and affect another.

Example: A Button Component with SCSS Modules

Let’s see it in action.

/* Button.module.scss */
/* Define variables for easy reuse */
$primary-color: #007bff;
$text-color: #ffffff;

/* Base button styles */
.button {
  border-radius: 8px;
  border: 1px solid transparent;
  padding: 0.6em 1.2em;
  font-size: 1em;
  font-weight: 500;
  cursor: pointer;
  transition: border-color 0.25s;
  background-color: #f9f9f9;
  color: #213547;
}

/* A modifier class for the primary variant */
.primary {
  background-color: $primary-color;
  color: $text-color;
}
// Button.jsx
import styles from './Button.module.scss';
import clsx from 'clsx'; // A utility for conditionally joining class names

// The 'primary' prop determines which style modifier to apply
export function Button({ primary, children, onClick }) {
  const buttonClasses = clsx(
    styles.button,
    primary && styles.primary // Apply styles.primary only if the 'primary' prop is true
  );

  return (
    <button className={buttonClasses} onClick={onClick}>
      {children}
    </button>
  );
}

Pros and Cons of Traditional Stylesheets

Pros Cons
- Clear Separation of Concerns: Style, logic, and structure live in different files, which many developers find clean and organized. - Context Switching: You have to jump between JS and SCSS files to understand a component fully.
- Standard Tooling: Works with standard CSS and preprocessors like Sass/SCSS, which have a mature ecosystem. - Class Name Management: Even with CSS Modules, you are still managing class name logic (e.g., using clsx or template literals).
- Static Analysis: CSS can be linted and analyzed independently of your JavaScript. - “Dead Code” Elimination: It can be difficult to confidently delete a style rule, as it’s hard to be certain it isn’t used somewhere in your application.
- Potentially Faster Initial Renders: All CSS can be bundled into a single .css file and loaded efficiently by the browser.

The Component-Driven Path: Co-location with Emotion

CSS-in-JS flips the traditional model on its head. The core philosophy is co-location: a component is a self-contained unit, and its styles are an integral part of its definition. Therefore, styles should live inside the component file.

Emotion is a leading CSS-in-JS library known for its high performance, small bundle size, and flexible API. It allows you to create React components that have styles attached to them.

How Emotion Works

Emotion uses JavaScript’s tagged template literals to let you write actual CSS syntax. At runtime (or build time, depending on configuration), it processes this CSS and injects it into the DOM with unique, generated class names. This provides automatic scoping without any extra setup.

The biggest advantage is the ability to use a component’s props directly within your style definitions, making dynamic styling incredibly intuitive.

Example: A Button Component with Emotion

Here is the same button, rebuilt with Emotion.

// Button.jsx
import styled from '@emotion/styled';

// All logic and styling live in this single file.
// The 'styled.button' syntax creates a React component that renders a <button> tag.
const StyledButton = styled.button`
  border-radius: 8px;
  border: 1px solid transparent;
  padding: 0.6em 1.2em;
  font-size: 1em;
  font-weight: 500;
  cursor: pointer;
  transition: border-color 0.25s;

  /* Use props directly inside the CSS! */
  background-color: ${props => (props.primary ? '#007bff' : '#f9f9f9')};
  color: ${props => (props.primary ? '#ffffff' : '#213547')};
`;

// The final component is clean and simple.
// It passes all its props (like 'primary') down to the StyledButton.
export function Button(props) {
  return <StyledButton {...props} />;
}

Pros and Cons of Emotion (CSS-in-JS)

Pros Cons
- True Encapsulation: All aspects of a component (logic, styles, structure) are in one place. When you delete the component, you delete its styles. No dead code. - Learning Curve: Requires learning a new library and paradigm.
- Intuitive Dynamic Styling: Using props to alter styles is simple and direct, eliminating the need for managing class name strings. - Runtime Overhead: A small runtime is needed to process styles, though libraries like Emotion are highly optimized.
- Confidence in Changes: Since styles are scoped locally, you can refactor a component’s styles without fear of breaking something else. - Breaks “Separation of Concerns”: For developers accustomed to traditional methods, mixing CSS in JS can feel unnatural.
- Excellent for Component Libraries: The encapsulation makes it the ideal choice for creating reusable, themeable components.

The Utility-First Path: Tailwind CSS

A third paradigm has rapidly gained popularity: Utility-First CSS. Instead of writing custom CSS rules for every component, you compose styles using pre-defined utility classes directly in your HTML/JSX. Tailwind CSS is the leader in this space.

How Tailwind Works

Tailwind scans your files for class names (e.g., flex, p-4, text-blue-500) and generates a minimal CSS file containing only the styles you actually used.

Example: A Button Component with Tailwind

// Button.jsx
// No import of external stylesheet or libraries needed (once Tailwind is setup)

export function Button({ primary, children, onClick }) {
  // We conditionall apply the blue background if 'primary' is true
  const baseClasses = "rounded-lg border border-transparent px-5 py-2 text-base font-medium cursor-pointer transition-colors";
  const modeClasses = primary
    ? "bg-blue-500 text-white hover:bg-blue-600"
    : "bg-gray-100 text-gray-800 hover:bg-gray-200";

  return (
    <button className={`${baseClasses} ${modeClasses}`} onClick={onClick}>
      {children}
    </button>
  );
}

Pros and Cons of Tailwind

Pros Cons
Speed: You rarely leave your HTML/JSX file. Styling feels incredibly fast and fluid. “Ugly” Markup: JSX can become cluttered with long strings of class names.
Consistency: You are constrained to a design system (spacing, colors) defined in your config, preventing “magic numbers”. Learning Curve: You must learn the utility class names (though VS Code extensions help immensely).
Performance: The final CSS bundle is tiny because it only contains used classes. Complex Selectors: Targeting pseudo-elements or complex children can be more verbose than standard CSS.

Head-to-Head Comparison

Aspect CSS / SCSS (with CSS Modules) Emotion (CSS-in-JS)
File Structure Styles in .scss, logic in .jsx. Styles and logic co-located in .jsx.
Style Scoping Opt-in via CSS Modules. Automatic and by default.
Dynamic Styling Manage conditional class names. Use props directly in style rules.
Dependencies sass preprocessor, clsx (optional). @emotion/react, @emotion/styled.
Key Philosophy Separation of Concerns. Co-location and Component Encapsulation.

Conclusion: Which Path Is Right for You?

There is no single “best” way to style a React application. The optimal choice depends on your project’s needs, your team’s experience, and your personal philosophy.

Choose Traditional CSS/SCSS if:

  • Your team has strong CSS skills and prefers a clear separation between styling and logic.
  • You are working on a project with a large, pre-existing CSS codebase.
  • You prioritize leveraging the browser’s native CSS parsing for the fastest possible initial paint.

Choose Emotion (CSS-in-JS) if:

  • You are building a design system or a reusable component library.
  • Your application features highly dynamic and complex UI that changes based on state.
  • Your team values component encapsulation and wants to eliminate the risk of style conflicts or unused CSS.