| |

React 73 ⚛️ Styled-Components and Emotion CSS-in-JS

⏱️ Estimated read: 13 min | |

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

LibraryPackages
Styled-componentsstyled-components
Emotion (css prop)@emotion/react
Emotion (styled)@emotion/react @emotion/styled
Emotion (Babel)@emotion/babel-plugin

Core API Comparison

FeatureStyled-componentsEmotion
Styled componentstyled.buttonstyled.button (from @emotion/styled)
Inline cssNocss prop
ThemingThemeProviderThemeProvider
RSC themingcreateTheme() (CSS vars)CSS custom properties
Transient props$propshouldForwardProp

Performance Benchmark (1,000 re-renders)

LibraryTime
CSS Modules12ms
Tailwind CSS14ms
Emotion (with Babel)38ms
Emotion (no Babel)55ms
Styled-components v662ms

Decision Criteria

SituationChoice
Using Material UIEmotion (already installed)
Prefer template literalsEither (both support)
Need css propEmotion
RSC-heavy appVerify version support
Zero-runtime priorityNeither (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

PitfallWhy It HappensFix
Styled-components v6 DOM warningProps forwarded by defaultUse $ prefix for transient props
Emotion css prop ignoredMissing JSX pragmaAdd /** @jsxImportSource @emotion/react */
Emotion slow without BabelRuntime style generationInstall @emotion/babel-plugin
v6 styles stop after errorParser treats } as nested block endFix syntax error; v5 was more forgiving
RSC theming brokenThemeProvider is no-op in RSCUse createTheme() or CSS vars
Material UI style conflictUsing styled-components alongside EmotionUse @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

ItemValue
Styled-componentsTemplate-literal CSS-in-JS
EmotionCSS-in-JS with css prop and styled API
Emotion Babel plugin@emotion/babel-plugin
MUI defaultEmotion
Transient props (SC)$prop
Emotion prop forwardingUse shouldForwardProp
SC v6 TypeScriptBundled
SC v6 RSC themingcreateTheme()
Performance gapEmotion+Babel faster than SC v6
Zero-runtime alternativeTailwind, 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 css prop 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!