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
.cssfiles for design ownership or tooling pipelines.
Additional practical examples (CSS Modules)
- 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>;
}
:globaland 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, andkeyframes. - Theming via ThemeProvider and
useThemehook. - 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:
renderToStringintegration 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
styledAPI (familiar and readable syntax). - Automatic critical CSS extraction support for SSR.
ThemeProviderand 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
styledAPI and theming, but Emotion also exposescssand 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.
Alternatives & related projects
- 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
- CSS Modules docs โ CSS Modules on GitHub
- Emotion docs โ Emotion docs
- Styled Components docs โ Styled Components docs
- Performance tooling โ
source-map-explorer,webpack-bundle-analyzer
Comments