Skip to main content
โšก Calmops

CSS Modules vs CSS-in-JS: Scoped Styling with Emotion and Styled Components

Modern front-end development moved from global CSS files to scoped styling approaches that reduce naming collisions, improve maintainability, and enable component-driven design. This article explains how CSS Modules and CSS-in-JS approaches (with a focus on Emotion and Styled Components) work, when to use each, and shows practical examples you can drop into your projects.


1) From global CSS to scoped styling โ€” short history

  • Global CSS era: large applications relied on global selectors and naming conventions (BEM, SMACSS) to avoid collisions.
  • Problems: cascade leakage, fragile selectors, and heavy global stylesheets that made long-term maintenance hard.
  • Solutions: CSS Modules and CSS-in-JS introduced scoping so styles belong to components, improving encapsulation and developer ergonomics.

Both approaches aim to solve the same core problem โ€” how to keep styles local to components while keeping the power of CSS.


Core terms & abbreviations (glossary)

  • SSR โ€” Server-Side Rendering: rendering HTML on the server to improve first paint and SEO.
  • JIT โ€” Just-in-Time compilation: on-demand generation of CSS utilities or rules (used by some modern CSS tools).
  • BEM โ€” Block-Element-Modifier: a CSS naming convention for organizing global styles.
  • CSSOM โ€” CSS Object Model: browser API representing CSS styles applied to the document.
  • Critical CSS โ€” CSS required for the initial render; often extracted during SSR to speed up first meaningful paint.

2) CSS Modules โ€” how it works and when to use it

What are CSS Modules?

CSS Modules are regular CSS files that, when imported into JavaScript, expose a mapping of class names that are locally scoped by default. Build tools (webpack, Vite) transform class names into hashed, unique identifiers so two components can use .button without collision.

Key features & benefits

  • Local scoping: class names are local to the module by default.
  • Familiar syntax: write plain CSS with selectors, pseudo-classes, and media queries.
  • Zero runtime library: generated class names are plain strings โ€” no runtime CSS-in-JS cost.

Example: using CSS Modules in React

/* Button.module.css */
.button {
  padding: 8px 12px;
  border-radius: 6px;
  background: var(--btn-bg, #2563eb);
  color: white;
}

.primary { background: var(--btn-primary, #2563eb); }
.secondary { background: #e5e7eb; color: #111827; }
// Button.jsx
import styles from './Button.module.css';

export default function Button({ children, variant = 'primary' }) {
  return (
    <button className={`${styles.button} ${styles[variant]}`}>{children}</button>
  );
}

When to choose CSS Modules

  • Your project prefers plain CSS (no runtime dependency).
  • You want predictable scoped styles with minimal setup.
  • You prefer to keep styling in separate .css files for design ownership or tooling pipelines.

Additional practical examples (CSS Modules)

  1. Theming with CSS variables + CSS Modules
/* theme.css (global variables) */
:root { --btn-bg: #2563eb; --btn-color: #fff; }

/* Button.module.css */
.button { padding: 8px 12px; border-radius: 6px; background: var(--btn-bg); color: var(--btn-color); }
.ghost { background: transparent; color: var(--btn-bg); }
// Button.jsx
import styles from './Button.module.css';
import './theme.css';

export function Button({ children, variant='primary' }) {
  return <button className={`${styles.button} ${variant === 'ghost' ? styles.ghost : ''}`}>{children}</button>;
}
  1. :global and composition

CSS Modules support local scoping but you can still reference global classes with :global(.classname) or compose using :local/composes depending on the loader.

/* banner.module.css */
.root { composes: container from './layout.module.css'; }
.title { font-size: 1.25rem; }
.link :global(a) { color: var(--link); } /* applies global anchor style inside scoped module */

These patterns let you combine the best parts of local scoping and shared design tokens.


3) CSS-in-JS โ€” idea and motivations

CSS-in-JS puts CSS declarations inside JavaScript, allowing dynamic styles, co-location of styles with components, theming via JS objects, and sometimes server-side rendering friendliness. This approach emerged to enable more dynamic styling and component encapsulation.

Benefits include:

  • Dynamic props-based styling (styles change based on component props)
  • Powerful theming and runtime composition
  • CSS capabilities (nesting, auto-prefixing) via JS APIs

Trade-offs:

  • Runtime cost (some libraries add small runtime overhead)
  • Larger bundle size if not optimized

4) Emotion โ€” features, API, and examples

What is Emotion?

Emotion is a performant CSS-in-JS library with two core APIs:

  • styled: component factory similar to styled-components
  • css / cx: low-level helpers for creating class names from style objects or strings

Emotion supports server-side rendering, theming, and zero-runtime optimization paths (when using the Babel plugin and extract options).

Emotion โ€” Key features

  • Flexible APIs: styled, css, and keyframes.
  • Theming via ThemeProvider and useTheme hook.
  • Good performance with caching and SSR options.

Example: basic styled component

npm install @emotion/react @emotion/styled
/** @jsxImportSource @emotion/react */
import styled from '@emotion/styled';

const Button = styled.button`
  padding: 8px 12px;
  border-radius: 6px;
  color: white;
  background: ${props => (props.primary ? '#2563eb' : '#6b7280')};
`;

export default function App() {
  return <Button primary>Save</Button>;
}

Example: css prop and theming

import { ThemeProvider } from '@emotion/react';
import { css } from '@emotion/react';

const theme = { colors: { primary: '#2563eb' } };

function Card() {
  return (
    <div css={css`padding:16px; border-radius:8px; background:#fff;`}>
      <h3 css={{ color: theme.colors.primary }}>Title</h3>
    </div>
  );
}

function App() {
  return (
    <ThemeProvider theme={theme}>
      <Card />
    </ThemeProvider>
  );
}

Performance & optimizations

  • Emotion generates atomic class names and caches styles. Use the Babel plugin (@emotion/babel-plugin) to extract static styles and reduce runtime cost.
  • Server-side rendering support: renderToString integration for SSR CSS extraction.

Practical example: animated spinner with Emotion

import styled from '@emotion/styled';
import { keyframes } from '@emotion/react';

const spin = keyframes`
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
`;

const Spinner = styled.div`
  width: 32px; height: 32px; border-radius: 50%; border: 3px solid #e5e7eb; border-top-color: #2563eb; animation: ${spin} 1s linear infinite;
`;

export default function Loading() { return <Spinner />; }

5) Styled Components โ€” features, API, and examples

What are Styled Components?

Styled Components popularized the tagged-template literal API for creating styled components and pioneered many CSS-in-JS patterns. It offers a styled factory similar to Emotion and built-in theming.

Styled Components โ€” Key features

  • Tagged template styled API (familiar and readable syntax).
  • Automatic critical CSS extraction support for SSR.
  • ThemeProvider and theming utilities.

Example: creating and using a styled component

npm install styled-components
import styled, { ThemeProvider } from 'styled-components';

const Button = styled.button`
  padding: 8px 12px;
  border-radius: 6px;
  color: white;
  background: ${props => (props.primary ? '#2563eb' : '#6b7280')};
`;

const theme = { colors: { primary: '#2563eb' } };

function App() {
  return (
    <ThemeProvider theme={theme}>
      <Button primary>Save</Button>
    </ThemeProvider>
  );
}

Differences vs Emotion

  • API parity: both provide styled API and theming, but Emotion also exposes css and optimized transforms.
  • Styled Components had earlier wide adoption; Emotion is often chosen for performance and flexibility.

6) Comparison: CSS Modules vs Emotion vs Styled Components

Factor CSS Modules Emotion Styled Components
Scoping Local by build step Local via generated classes Local via generated classes
Runtime No runtime cost Small runtime; can be optimized Small runtime
Dynamic styles Limited (via class toggles) Excellent (props & css prop) Excellent (props & tagged templates)
Theming Implement with CSS variables or utility classes Built-in ThemeProvider Built-in ThemeProvider
SSR Works (static CSS) Good SSR support Good SSR support
Tooling Familiar CSS tooling Babel plugin improves output Auto-extraction for SSR

7) Performance considerations

  • CSS Modules: minimal runtime overhead; ideal when you want near-zero runtime cost.
  • Emotion / Styled Components: some runtime cost for generating class names and handling dynamic styles; mitigations include Babel plugins, caching, and static extraction.
  • Measure actual impact with bundle analyzers (source-map-explorer, rollup-plugin-visualizer) and performance profiling.

Deployment & SSR flow (text diagram)

When you use server-side rendering with Emotion or Styled Components, your build/deploy flow commonly looks like:

developer -> git repo -> CI (build + SSR CSS extraction) -> CDN/Edge -> browser -> backend APIs

Make sure SSR CSS extraction runs during CI so the initial HTML includes critical CSS and reduces FOUS (flash of unstyled content).

8) Best practices & common pitfalls

  • Prefer static styles when possible: static CSS is faster and more predictable.
  • Co-locate styles thoughtfully: co-locating enables easier maintenance, but large style blocks inside JS may be harder to scan.
  • Use CSS variables for theme tokens when integrating CSS Modules with runtime theme changes.
  • Avoid excessive dynamic styles โ€” prefer class toggles or utilities when possible to reduce runtime work.
  • Test SSR and hydration flows if using CSS-in-JS; ensure critical CSS is extracted for performance.

Common pitfalls (expanded)

  • Unbounded dynamic styles: generating many unique style values at runtime (e.g., colors from user input) can lead to many classes and memory pressure; prefer reusing tokens or clamping inputs.
  • Complex selectors in CSS Modules: deeply nested selectors negate the locality benefit and can be brittle; prefer shallow, semantic selectors.
  • Assuming CSS-in-JS removes CSS knowledge: you still need to understand cascade, specificity, and browser rendering to avoid performance and layout issues.
  • Mixing too many styling approaches: mixing global CSS, CSS Modules, and CSS-in-JS across the same project without rules leads to confusionโ€”establish team conventions early.

  • Tailwind CSS โ€” utility-first CSS framework that avoids much of CSS-in-JS by composing utilities in markup: Tailwind CSS
  • Stitches โ€” a modern CSS-in-JS library focused on performance and developer ergonomics: Stitches
  • Vanilla Extract โ€” compile-time CSS-in-TypeScript: Vanilla Extract
  • Linaria โ€” zero-runtime CSS-in-JS that extracts CSS at build time: Linaria
  • WindiCSS / UnoCSS โ€” on-demand utility generators (alternatives to Tailwind) useful in certain workflows.


9) Recommendations โ€” when to use which

  • Use CSS Modules when you want simple, familiar CSS with local scoping and minimal runtime overhead.
  • Use Emotion when you need powerful dynamic styling, theming, and good performance with available optimizations.
  • Use Styled Components if you want a clean tagged-template API, built-in SSR support, and a mature ecosystem.

For many teams, the decision comes down to team familiarity and ecosystem: choose the approach that aligns with your tooling (e.g., Tailwind + CSS Modules vs component libraries that use Emotion/MUI).


10) Further reading & resources


Comments