React 73 ⚛️ Styled-Components and Emotion CSS-in-JS
CSS-in-JS libraries solve a problem that plain CSS and CSS Modules do not: they let styles depend on JavaScript values—props, state, theme context—without generating an explosion of class names or requiring inline style objects. The two dominant libraries in this space are styled-components and Emotion. They share the same core idea: you write CSS in JavaScript, and the library generates scoped class names at runtime and injects the styles into the document.
The differences between them are practical rather than philosophical. Styled-components has a larger legacy codebase and a template-literal-first API. Emotion is the engine behind Material UI, ships with a css prop for inline styling, and has a Babel plugin that moves style generation to build time for a measurable performance win . Both support React Server Components to varying degrees, both have TypeScript types, and both add runtime overhead compared to zero-runtime alternatives like Tailwind or CSS Modules.
This chapter covers the setup and core APIs of both libraries, the performance characteristics that matter in production, and the decision criteria for choosing one over the other.
Key point: Styled-components and Emotion both generate scoped class names at runtime from JavaScript-defined styles; Emotion offers a faster path when its Babel plugin is enabled and is the default in Material UI, while styled-components offers a mature template-literal API and RSC-compatible theming via CSS variables.
Why CSS-in-JS libraries still exist
The dynamic styling problem. CSS Modules and Tailwind are static: the class names exist at build time, and the values are fixed in CSS files. When a style depends on a prop—a button whose background color comes from a theme object or a user preference—you either generate a class for every possible value or fall back to inline styles. CSS-in-JS libraries solve this by making the style definition a JavaScript function of props, and generating a class name only for the combinations that actually render .
The theming problem. A design system needs tokens—colors, spacing, typography—that components consume without knowing where they come from. CSS-in-JS libraries provide a ThemeProvider that passes the theme through React context, and styled components read props.theme directly in their template literals . This is more ergonomic than CSS custom properties for complex theme objects, though custom properties are the recommended approach for React Server Components .
The component encapsulation problem. A styled component is a React component with styles attached. It encapsulates the markup and the styling in one unit, and the generated class name is unique to that component. There is no risk of a .button class leaking into another component, and no naming convention to enforce .
The runtime cost problem. The reason CSS-in-JS is declining in popularity is that it ships styles inside JavaScript. Every styled component adds bytes to the bundle, and every render may generate new class names and inject new CSS rules. A production comparison showed styled-components adding roughly 20% to first-load JavaScript, with Emotion performing slightly better . Benchmarks of 1,000 re-renders with dynamic props showed Emotion at 38ms with its Babel plugin and 55ms without, versus styled-components at 62ms . The gap is meaningful for high-frequency renders but negligible for typical UI interactions.
a. Styled-components: setup and core API
Installation is a single package. Since v6, TypeScript types are bundled, so the @types/styled-components package is no longer needed .
npm install styled-components
The core API is styled.tag, which returns a React component with the styles attached. The template literal contains CSS, and interpolations are functions that receive props:
import styled from 'styled-components'
const Button = styled.button`
background: ${props => props.$primary ? 'palevioletred' : 'white'};
color: ${props => props.$primary ? 'white' : 'palevioletred'};
padding: 0.5em 1em;
border: 2px solid palevioletred;
border-radius: 4px;
`
function App() {
return (
<>
<Button>Normal</Button>
<Button $primary>Primary</Button>
</>
)
}
The $ prefix on $primary marks it as a transient prop. Styled-components v6 does not filter props by default, so without the prefix, primary would be forwarded to the DOM and React would warn about an unknown attribute . The $ convention is the recommended fix.
Theming uses ThemeProvider:
import styled, { ThemeProvider } from 'styled-components'
const theme = { fg: 'palevioletred', bg: 'white' }
const Card = styled.div`
background: ${props => props.theme.bg};
color: ${props => props.theme.fg};
padding: 2em;
`
function App() {
return (
<ThemeProvider theme={theme}>
<Card>Themed content</Card>
</ThemeProvider>
)
}
For React Server Components, ThemeProvider is a no-op. Styled-components v6 provides createTheme(), which converts theme tokens into CSS custom properties and emits them via a GlobalStyle component. The class name hashes remain stable across theme variants, avoiding hydration mismatches .
A v6 change that matters for debugging: a syntax error in one line of a styled block now causes all subsequent styles in that block to be ignored, whereas v5 silently skipped the invalid line and applied the rest . This makes typos more visible but also more disruptive.
b. Emotion: setup and core API
Emotion has two React entry points. @emotion/react provides the css prop and theming. @emotion/styled provides the styled API for teams that prefer the styled-components pattern .
npm install @emotion/react @emotion/styled
The css prop styles an element directly without creating a component:
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
const style = css`
color: hotpink;
padding: 1em;
`
function App() {
return <div css={style}>Styled with the css prop</div>
}
The /** @jsxImportSource @emotion/react */ pragma tells the JSX transform to use Emotion’s jsx function, which handles the css prop. Without it, the prop is passed through to the DOM. In a Vite or Next.js project, the pragma can be configured globally so it does not appear in every file.
The styled API mirrors styled-components:
import styled from '@emotion/styled'
const Button = styled.button`
background: ${props => props.primary ? 'hotpink' : 'turquoise'};
color: white;
padding: 0.5em 1em;
`
Emotion supports both template literals and object styles. The object form is useful for TypeScript because camelCase properties are type-checked:
const style = css({
backgroundColor: 'hotpink',
padding: '1em'
})
The performance lever for Emotion is @emotion/babel-plugin. Without it, Emotion generates class names and serializes styles on every render. With it, static styles are extracted at build time and dynamic styles are cached. The benchmark difference is 38ms versus 55ms for 1,000 re-renders .
c. Choosing between them and the performance trade-off
The decision is rarely about raw performance for typical applications. Both libraries add runtime overhead, and both are slower than zero-runtime alternatives. The benchmark table from pkgpulse shows CSS Modules at 12ms, Tailwind at 14ms, Emotion with Babel at 38ms, and styled-components at 62ms for the same re-render workload . If you are rendering a virtualized table with 10,000 rows and dynamic per-row styles, neither library is the right choice—use CSS Modules or Tailwind.
For standard dashboard UIs, the choice comes down to ecosystem and API preference. The largest practical factor is Material UI. MUI v5 uses Emotion as its default styling engine, so any project using MUI already has Emotion installed . Adding custom Emotion styles in a MUI codebase is free; adding styled-components means running two styling systems with separate theming primitives. If you are not using MUI, styled-components remains a mature choice with a template-literal API that feels closer to real CSS.
Both libraries are working on React Server Components compatibility. Styled-components v6 added createTheme() for CSS-variable-based theming that works in RSC environments . Emotion’s css prop and theming work in RSC with the appropriate Babel configuration, though ThemeProvider is a no-op in RSC and custom properties are the recommended path . If RSC is a hard requirement, verify the specific library version’s support before committing.
Complete Example Session
The following session builds the same themed card component in both libraries, demonstrating the API differences.
// ============================================
// PART 1: STYLED-COMPONENTS SETUP
// ============================================
import styled, { ThemeProvider } from 'styled-components'
const theme = {
colors: { primary: '#6366f1', surface: '#ffffff', text: '#1f2937' },
space: { md: '1rem', lg: '1.5rem' }
}
// ============================================
// PART 2: STYLED-COMPONENTS CARD
// ============================================
const SCTitle = styled.h2`
color: ${props => props.theme.colors.text};
font-size: 1.25rem;
margin: 0 0 ${props => props.theme.space.md};
`
const SCCard = styled.div`
background: ${props => props.theme.colors.surface};
border: 1px solid ${props => props.theme.colors.primary};
border-radius: 8px;
padding: ${props => props.theme.space.lg};
`
function StyledCard({ title, children }) {
return (
<SCCard>
<SCTitle>{title}</SCTitle>
{children}
</SCCard>
)
}
// ============================================
// PART 3: STYLED-COMPONENTS APP
// ============================================
function StyledApp() {
return (
<ThemeProvider theme={theme}>
<StyledCard title="Styled-components">Content</StyledCard>
</ThemeProvider>
)
}
// ============================================
// PART 4: EMOTION SETUP
// ============================================
/** @jsxImportSource @emotion/react */
import { css, ThemeProvider as EmotionThemeProvider } from '@emotion/react'
import styledEmotion from '@emotion/styled'
// ============================================
// PART 5: EMOTION CARD (STYLED API)
// ============================================
const EmTitle = styledEmotion.h2`
color: ${props => props.theme.colors.text};
font-size: 1.25rem;
margin: 0 0 ${props => props.theme.space.md};
`
const EmCard = styledEmotion.div`
background: ${props => props.theme.colors.surface};
border: 1px solid ${props => props.theme.colors.primary};
border-radius: 8px;
padding: ${props => props.theme.space.lg};
`
// ============================================
// PART 6: EMOTION CARD (CSS PROP)
// ============================================
function EmotionCard({ title, children }) {
return (
<div css={theme => ({
background: theme.colors.surface,
border: `1px solid ${theme.colors.primary}`,
borderRadius: 8,
padding: theme.space.lg
})}>
<h2 css={theme => ({
color: theme.colors.text,
fontSize: '1.25rem',
margin: `0 0 ${theme.space.md}`
})}>
{title}
</h2>
{children}
</div>
)
}
// ============================================
// PART 7: EMOTION APP
// ============================================
function EmotionApp() {
return (
<EmotionThemeProvider theme={theme}>
<EmotionCard title="Emotion">Content</EmotionCard>
</EmotionThemeProvider>
)
}
// ============================================
// PART 8: SHARED THEME OBJECT
// ============================================
// Both libraries can consume the same theme object,
// making incremental migration possible.
// ============================================
// PART 9: DYNAMIC PROP COMPARISON
// ============================================
const SCButton = styled.button`
background: ${props => props.$active ? '#6366f1' : '#e5e7eb'};
`
const EmButton = styledEmotion.button`
background: ${props => props.active ? '#6366f1' : '#e5e7eb'};
`
// Styled-components uses $ prefix for transient props (v6).
// Emotion forwards props to the DOM; use `shouldForwardProp` if needed.
// ============================================
// PART 10: BABEL PLUGIN (EMOTION)
// ============================================
// In babel.config.js or .babelrc:
// {
// "plugins": ["@emotion/babel-plugin"]
// }
// This enables build-time optimization and source maps.
The ten parts cover styled-components setup, card creation, app wiring, Emotion setup, the styled API, the css prop, Emotion app wiring, the shared theme object, dynamic prop differences, and the Emotion Babel plugin. What the session demonstrates is that the APIs are nearly parallel—styled-components uses $ for transient props, Emotion forwards props unless configured otherwise, and the css prop is Emotion’s distinguishing feature.
Quick Reference
Package Installation
| Library | Packages |
|---|---|
| Styled-components | styled-components |
| Emotion (css prop) | @emotion/react |
| Emotion (styled) | @emotion/react @emotion/styled |
| Emotion (Babel) | @emotion/babel-plugin |
Core API Comparison
| Feature | Styled-components | Emotion |
|---|---|---|
| Styled component | styled.button | styled.button (from @emotion/styled) |
| Inline css | No | css prop |
| Theming | ThemeProvider | ThemeProvider |
| RSC theming | createTheme() (CSS vars) | CSS custom properties |
| Transient props | $prop | shouldForwardProp |
Performance Benchmark (1,000 re-renders)
| Library | Time |
|---|---|
| CSS Modules | 12ms |
| Tailwind CSS | 14ms |
| Emotion (with Babel) | 38ms |
| Emotion (no Babel) | 55ms |
| Styled-components v6 | 62ms |
Decision Criteria
| Situation | Choice |
|---|---|
| Using Material UI | Emotion (already installed) |
| Prefer template literals | Either (both support) |
Need css prop | Emotion |
| RSC-heavy app | Verify version support |
| Zero-runtime priority | Neither (use Tailwind/CSS Modules) |
Best Practices
✅ Do This:
// Styled-components: use $ for transient props
<Button $primary>Click</Button>
// Emotion: enable the Babel plugin
// babel.config.js: plugins: ['@emotion/babel-plugin']
// Both: extract static styles to top-level
const Title = styled.h1` font-size: 2rem; `;
// Emotion: use css prop for one-off overrides
<div css={css` margin-top: 1rem; `} />
❌ Don’t Do This:
// Styled-components: forwarding unknown props to DOM
<Button primary>Click</Button> // React warning
// Emotion: using css prop without JSX pragma
<div css={{ color: 'red' }} /> // ignored without @jsxImportSource
// Declaring styled components inside render
function App() {
const Title = styled.h1``; // new component every render
}
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Styled-components v6 DOM warning | Props forwarded by default | Use $ prefix for transient props |
Emotion css prop ignored | Missing JSX pragma | Add /** @jsxImportSource @emotion/react */ |
| Emotion slow without Babel | Runtime style generation | Install @emotion/babel-plugin |
| v6 styles stop after error | Parser treats } as nested block end | Fix syntax error; v5 was more forgiving |
| RSC theming broken | ThemeProvider is no-op in RSC | Use createTheme() or CSS vars |
| Material UI style conflict | Using styled-components alongside Emotion | Use @mui/styled-engine-sc or unify on Emotion |
Real-World Examples
1. Styled-components button
const Button = styled.button`
background: ${p => p.$primary ? 'blue' : 'gray'};
`
2. Emotion css prop
<div css={{ display: 'flex', gap: '1rem' }} />
3. Styled-components theme
<ThemeProvider theme={theme}><App /></ThemeProvider>
4. Emotion theme
<ThemeProvider theme={theme}><App /></ThemeProvider>
5. Emotion Babel plugin
{ "plugins": ["@emotion/babel-plugin"] }
6. Styled-components v6 transient prop
<Button $active>Active</Button>
7. Emotion shouldForwardProp
const Button = styled.button({ shouldForwardProp: prop => prop !== 'active' })`
background: ${p => p.active ? 'blue' : 'gray'};
`
8. Styled-components createTheme for RSC
const { theme, GlobalStyle } = createTheme({ colors: { fg: 'red' } })
9. Emotion object styles (TypeScript)
const style = css({ backgroundColor: 'hotpink' })
10. MUI default (Emotion)
npm install @mui/material @emotion/react @emotion/styled
Visual
Runtime CSS-in-JS Flow
┌──────────────────────────────────────────────┐
│ Component renders with props │
│ │ │
│ ▼ │
│ Styled component evaluates template │
│ or css prop object │
│ │ │
│ ▼ │
│ Library hashes styles → class name │
│ │ │
│ ▼ │
│ Injects <style> tag if new │
│ │ │
│ ▼ │
│ Returns className for React to apply │
└──────────────────────────────────────────────┘
Emotion vs Styled-Components API
┌──────────────────────────────────────────────┐
│ STYLED-COMPONENTS │
│ styled.button`...` │
│ $prop for transient │
│ ThemeProvider (context) │
│ │
│ EMOTION │
│ styled.button`...` (from @emotion/styled) │
│ css={{ ... }} prop on any element │
│ ThemeProvider (context) │
│ Babel plugin for build-time optimization │
└──────────────────────────────────────────────┘
Performance Comparison
┌──────────────────────────────────────────────┐
│ 1,000 re-renders with dynamic props │
│ │
│ CSS Modules ████ 12ms │
│ Tailwind █████ 14ms │
│ Emotion+Babel ████████████ 38ms │
│ Emotion (no) ██████████████████ 55ms │
│ styled-comp v6 ████████████████████ 62ms │
│ │
│ Zero-runtime alternatives are 3-5× faster │
└──────────────────────────────────────────────┘
When to Choose
┌──────────────────────────────────────────────┐
│ Using MUI? │
│ ├─ YES → Emotion (already installed) │
│ └─ NO → Need css prop? │
│ ├─ YES → Emotion │
│ └─ NO → Prefer template literals? │
│ ├─ YES → styled-components │
│ └─ NO → Consider zero-runtime│
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Styled-components | Template-literal CSS-in-JS |
| Emotion | CSS-in-JS with css prop and styled API |
| Emotion Babel plugin | @emotion/babel-plugin |
| MUI default | Emotion |
| Transient props (SC) | $prop |
| Emotion prop forwarding | Use shouldForwardProp |
| SC v6 TypeScript | Bundled |
| SC v6 RSC theming | createTheme() |
| Performance gap | Emotion+Babel faster than SC v6 |
| Zero-runtime alternative | Tailwind, CSS Modules |
Key takeaways:
- Both libraries generate scoped class names at runtime. The class name is derived from a hash of the styles, and the CSS is injected into the document .
- Emotion’s
cssprop avoids creating a component. It styles an element inline with full CSS support, which is useful for one-off overrides . - The Emotion Babel plugin is the biggest performance lever. Without it, styles are regenerated on every render; with it, static styles are extracted at build time .
- Styled-components v6 requires
$for transient props. Without the prefix, props are forwarded to the DOM and React warns . - Material UI uses Emotion. In a MUI codebase, choosing styled-components means running two styling systems .
- Both add runtime overhead. For high-frequency renders or performance-critical UIs, CSS Modules or Tailwind are faster .
- RSC compatibility is version-dependent. Styled-components v6 and recent Emotion versions have workarounds, but the safest path in RSC is CSS custom properties .
Remember: Styled-components and Emotion are the two mature CSS-in-JS libraries for React, and they solve the same problem: styles that depend on JavaScript values. The APIs are nearly parallel—styled.tag in both, with Emotion adding a css prop and styled-components enforcing $ transient props in v6. The decision is usually driven by ecosystem (MUI uses Emotion) or API preference (template literals versus the css prop). For new projects without MUI, the performance difference is small enough that either is defensible, but the runtime cost is real: neither is as fast as zero-runtime alternatives like Tailwind or CSS Modules. If you choose one, enable the Babel plugin for Emotion, or accept the runtime generation cost for styled-components.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!