Emotion is great until you turn on a real Content Security Policy and your app suddenly looks naked.

That’s the usual collision: Emotion injects styles at runtime with <style> tags, while CSP blocks inline styles unless you explicitly allow them. A lot of teams “fix” that by adding 'unsafe-inline' to style-src, which defeats a big chunk of why you enabled CSP in the first place.

If you’re using Emotion on a production app, the right fix is usually a nonce-based CSP and making sure Emotion attaches that nonce to every injected style tag.

Why Emotion breaks under CSP

Emotion generates CSS and inserts it into the document at runtime. From CSP’s perspective, those are inline styles. If your policy looks like this:

Content-Security-Policy: default-src 'self'; style-src 'self'; script-src 'self';

the browser will block Emotion’s injected styles because style-src 'self' does not permit inline <style> elements.

A lot of live sites still run policies like this real one from headertest.com:

content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-NzMzMDBjM2YtOTMxOC00MzQ5LThjNDItY2NlZTBkNjg4Y2M4' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com; frame-src 'self' https://consentcdn.cookiebot.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'

Notice the style-src 'unsafe-inline'. That’s often there because of CSS-in-JS libraries, tag managers, consent tools, or all three.

For Emotion, you can usually do better.

The secure approach

You want three things:

  1. Generate a fresh nonce per request
  2. Put that nonce in your CSP header under style-src
  3. Pass the same nonce to Emotion so its <style> tags include nonce="..."

Then the browser allows only style tags carrying your server-generated nonce.

Your policy ends up looking more like this:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-RANDOM_NONCE' 'strict-dynamic';
  style-src 'self' 'nonce-RANDOM_NONCE';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  form-action 'self';

If you need a refresher on directive behavior, https://csp-guide.com is a solid reference, and the official CSP spec/docs are worth keeping open too.

Emotion’s nonce support

Emotion supports adding a nonce through its cache configuration. That’s the key piece.

import createCache from '@emotion/cache'

const cache = createCache({
  key: 'app',
  nonce: 'RANDOM_NONCE'
})

Any style tags inserted by that cache will include the nonce.

Basic React setup

Here’s the simplest client-side pattern.

1. Server: generate a nonce and send CSP

Example using Express:

import express from 'express'
import crypto from 'crypto'

const app = express()

app.use((req, res, next) => {
  const nonce = crypto.randomBytes(16).toString('base64')
  res.locals.nonce = nonce

  const csp = [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
    `style-src 'self' 'nonce-${nonce}'`,
    "img-src 'self' data: https:",
    "font-src 'self'",
    "connect-src 'self'",
    "object-src 'none'",
    "base-uri 'self'",
    "frame-ancestors 'none'",
    "form-action 'self'",
  ].join('; ')

  res.setHeader('Content-Security-Policy', csp)
  next()
})

2. Render the nonce into HTML

You need the client app to read the nonce somehow. A meta tag works fine:

<meta name="csp-nonce" content="{{nonce}}">
<div id="root"><!-- app --></div>

3. Create an Emotion cache with the nonce

import React from 'react'
import ReactDOM from 'react-dom/client'
import { CacheProvider } from '@emotion/react'
import createCache from '@emotion/cache'
import App from './App'

const nonce =
  document.querySelector('meta[name="csp-nonce"]')?.getAttribute('content') || ''

const cache = createCache({
  key: 'app',
  nonce,
})

ReactDOM.createRoot(document.getElementById('root')!).render(
  <CacheProvider value={cache}>
    <App />
  </CacheProvider>
)

That’s the core fix. If Emotion owns style injection through that cache, your CSP can stay strict without unsafe-inline.

Example component

A small Emotion example:

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'

export function Button() {
  return (
    <button
      css={css`
        background: #111827;
        color: white;
        border: 0;
        padding: 0.75rem 1rem;
        border-radius: 8px;

        &:hover {
          background: #1f2937;
        }
      `}
    >
      Save changes
    </button>
  )
}

Without the nonce-aware cache, the generated <style> tag gets blocked. With the cache, it renders normally.

SSR with Emotion

If you server-render React, you need to think about both:

  • the CSP header
  • Emotion’s SSR output

A typical SSR flow creates a cache per request, not globally. That matters because the nonce is per request too.

import createCache from '@emotion/cache'
import createEmotionServer from '@emotion/server/create-instance'
import { CacheProvider } from '@emotion/react'
import { renderToString } from 'react-dom/server'
import App from './App'

export function renderPage(nonce: string) {
  const cache = createCache({
    key: 'app',
    nonce,
  })

  const { extractCriticalToChunks, constructStyleTagsFromChunks } =
    createEmotionServer(cache)

  const html = renderToString(
    <CacheProvider value={cache}>
      <App />
    </CacheProvider>
  )

  const chunks = extractCriticalToChunks(html)
  const styles = constructStyleTagsFromChunks(chunks)

  return `
    <!doctype html>
    <html>
      <head>
        <meta name="csp-nonce" content="${nonce}">
        ${styles}
      </head>
      <body>
        <div id="root">${html}</div>
      </body>
    </html>
  `
}

Because the cache is configured with nonce, the generated SSR <style> tags include it too.

One warning: don’t reuse a single Emotion cache across requests on the server. You’ll eventually leak state or get the wrong nonce attached.

Next.js pattern

If you’re using Next.js, the same rule applies: generate a nonce per request, set the CSP header, and pass the nonce into Emotion’s cache.

A minimal cache helper:

// createEmotionCache.ts
import createCache from '@emotion/cache'

export default function createEmotionCache(nonce?: string) {
  return createCache({
    key: 'css',
    nonce,
  })
}

Client setup:

import { CacheProvider } from '@emotion/react'
import { useMemo } from 'react'
import createEmotionCache from './createEmotionCache'

export default function App({ Component, pageProps, nonce }) {
  const cache = useMemo(() => createEmotionCache(nonce), [nonce])

  return (
    <CacheProvider value={cache}>
      <Component {...pageProps} />
    </CacheProvider>
  )
}

Then inject nonce from your document or server layer. The exact wiring depends on whether you’re in the Pages Router or App Router, but the principle doesn’t change.

When people accidentally break this

I’ve seen the same mistakes over and over.

1. Nonce in CSP, but not in Emotion cache

You set:

style-src 'self' 'nonce-abc123'

but Emotion still creates:

<style data-emotion="app ...">

instead of:

<style nonce="abc123" data-emotion="app ...">

Result: blocked styles.

2. Static nonce

Don’t hardcode a nonce in config or env. A nonce must be unpredictable and generated per response.

Bad:

const nonce = process.env.CSP_NONCE

Good:

const nonce = crypto.randomBytes(16).toString('base64')

3. Falling back to unsafe-inline

If you leave this in place:

style-src 'self' 'unsafe-inline'

your nonce setup is mostly wasted for style protection. The browser will allow any inline style block anyway.

4. Third-party tools forcing inline styles

This is where things get annoying. Consent managers, A/B testing tools, and tag managers sometimes inject their own styles. That’s why real-world policies, like the headertest.com example, often keep style-src 'unsafe-inline'.

If you fully control the app shell and Emotion is your only inline style source, remove unsafe-inline. If you depend on external tools that inject anonymous inline styles, you may have to keep it until those integrations are cleaned up.

Be honest about the tradeoff.

A migration path from unsafe-inline

If your current policy looks like:

style-src 'self' 'unsafe-inline';

I’d migrate like this:

  1. Add nonce generation on every request
  2. Pass nonce into Emotion cache
  3. Change policy to:
   style-src 'self' 'nonce-...';
  1. Test every route, modal, lazy-loaded component, and hydration path
  2. Check browser CSP violation reports or devtools console
  3. Only re-add exceptions if a real dependency requires them

That last step matters. Don’t cargo-cult your CSP from old snippets.

Debugging checklist

When Emotion styles are blocked, inspect the DOM and the response.

Check the CSP header:

style-src 'self' 'nonce-XYZ'

Check the style tag:

<style nonce="XYZ" data-emotion="app ...">

If those values don’t match exactly, the browser will block it.

Also check for multiple caches. If part of your app uses one Emotion cache with a nonce and another subtree uses the default cache without one, you’ll get inconsistent failures that are miserable to debug.

A practical policy for an Emotion app

For a fairly normal React app using Emotion and no third-party style injectors, I’d start here:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-RANDOM_NONCE' 'strict-dynamic';
  style-src 'self' 'nonce-RANDOM_NONCE';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://api.example.com;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  form-action 'self';

That’s much tighter than the common “just add unsafe-inline and move on” approach.

Official docs worth checking

For exact Emotion APIs, check the official Emotion documentation. For CSP behavior and browser rules, use the official MDN and CSP spec docs. If you want a practical breakdown of directives and examples, https://csp-guide.com is useful.

The big idea is simple: Emotion and CSP work fine together if you treat the nonce as part of your rendering pipeline. Generate it per request, include it in the header, and give the same value to Emotion’s cache. That’s the difference between a strict policy and a fake one.