CSS-in-JS is great right up until you try to lock down style-src.
That’s where the friction starts. Many CSS-in-JS libraries inject <style> tags at runtime, and CSP treats those as inline styles. If your policy is strict, those styles get blocked unless you loosen style-src or add a nonce.
I’ve seen teams spend weeks tightening script-src only to quietly leave style-src 'unsafe-inline' in place because Emotion, styled-components, JSS, or a legacy UI kit needed it. That usually happens because CSS-in-JS was adopted for developer experience, while CSP got added later under security pressure.
Here’s the practical comparison guide I wish more frontend teams had before they shipped both.
The core problem
A strict CSP often starts here:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-r4nd0m';
style-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
Looks good, but many CSS-in-JS libraries will break because they generate styles dynamically and insert them into the DOM.
To make them work, teams usually pick one of these approaches:
- Allow
style-src 'unsafe-inline' - Use a nonce on generated
<style>tags - Render styles server-side with a nonce
- Avoid runtime injection by extracting CSS at build time
- Mix approaches and accept some debt
Each one has tradeoffs.
Option 1: style-src 'unsafe-inline'
This is the most common escape hatch.
A real-world example from headertest.com includes:
content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-YmQ2Yjg1ZjctZGIwNS00MWQyLTgzYmEtNDljZmJkZjViNjBi' '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'
That policy is doing a lot of good things, especially around script-src with a nonce and strict-dynamic. But style-src 'unsafe-inline' is still a meaningful concession.
Pros
- Easiest way to make CSS-in-JS work
- No framework changes in many setups
- Works with third-party widgets that inject styles
- Low engineering cost
Cons
- Weakens CSP meaningfully for styles
- Inline style injection becomes allowed
- Makes it harder to claim you have a truly strict CSP
- Often becomes permanent because nobody wants to untangle it later
My opinion: this is acceptable as a temporary migration step, not a destination.
If you need a refresher on how style-src behaves, https://csp-guide.com/style-src/ is a good reference.
Option 2: Use nonces for runtime-generated styles
This is the cleanest path when your CSS-in-JS library supports attaching a nonce to injected <style> tags.
The policy becomes:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-abc123';
style-src 'self' 'nonce-abc123';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
Then your app sets the same nonce on every server-approved <style> tag.
Emotion example
import createCache from '@emotion/cache';
import { CacheProvider } from '@emotion/react';
const cache = createCache({
key: 'app',
nonce: window.__CSP_NONCE__,
});
export function AppProviders({ children }) {
return <CacheProvider value={cache}>{children}</CacheProvider>;
}
styled-components example with StyleSheetManager
import { StyleSheetManager } from 'styled-components';
export function AppProviders({ children }) {
return (
<StyleSheetManager nonce={window.__CSP_NONCE__}>
{children}
</StyleSheetManager>
);
}
Pros
- Stronger than
unsafe-inline - Works well with modern CSS-in-JS libraries
- Keeps dynamic styling possible
- Good fit for SSR apps already generating request-scoped values
Cons
- Every response needs a fresh nonce
- You need to plumb that nonce through SSR, hydration, and client boot
- Easy to break during framework upgrades
- Some libraries or old versions don’t support nonce handling well
This is the option I recommend most often for teams that really want runtime CSS-in-JS.
Option 3: SSR with nonce-bearing style tags
If you already render styles on the server, you can attach the nonce during SSR and avoid some client-side surprises.
Example in Express
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64');
res.setHeader(
'Content-Security-Policy',
[
"default-src 'self'",
`script-src 'self' 'nonce-${res.locals.nonce}'`,
`style-src 'self' 'nonce-${res.locals.nonce}'`,
"object-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'",
].join('; ')
);
next();
});
app.get('/', (req, res) => {
const nonce = res.locals.nonce;
res.send(`
<!doctype html>
<html>
<head>
<meta name="csp-nonce" content="${nonce}">
<style nonce="${nonce}">body{font-family:sans-serif}</style>
</head>
<body>
<div id="root"></div>
<script nonce="${nonce}">
window.__CSP_NONCE__ = "${nonce}";
</script>
<script nonce="${nonce}" src="/app.js"></script>
</body>
</html>
`);
});
Pros
- Strong CSP without
unsafe-inline - Predictable when done fully on the server
- Good fit for Next.js, Remix, custom React SSR, and similar stacks
Cons
- More moving parts than people expect
- Hydration mismatches can appear if client-side injection uses a different setup
- Third-party components can still force exceptions
This works best when the whole rendering pipeline is under your control.
Option 4: Extract CSS at build time
This is the “stop fighting the browser” option.
Instead of injecting styles at runtime, use a library or build mode that emits static CSS files:
<link rel="stylesheet" href="/assets/app.css">
Then your policy can stay simple:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-abc123';
style-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
Pros
- Best CSP compatibility
- No inline style exceptions needed
- Better cacheability in many cases
- Easier to reason about in audits
Cons
- You lose some runtime flexibility
- The developer experience may change
- Theme systems based on dynamic generation may need refactoring
- Not every CSS-in-JS library supports true extraction equally well
From a security perspective, this is the cleanest answer. From a product engineering perspective, it can be expensive if your design system was built around runtime styling.
Option 5: Hybrid setup
This is what many production apps actually do.
Example:
- extracted CSS for the app shell
- nonce-based runtime styles for a few components
unsafe-inlinestill left in place for a consent manager or old widget
That’s often where teams land after the first CSP rollout.
Pros
- Pragmatic
- Lets you reduce risk incrementally
- Easier migration path from legacy stacks
Cons
- Harder to document
- Easy to forget why exceptions exist
- Can leave permanent weak spots in
style-src
If you go hybrid, document every exception and put an owner on it. Otherwise “temporary” turns into three years.
Library-by-library practical guidance
Emotion
Usually one of the easier libraries to make CSP-friendly because nonce support is straightforward through the cache.
Best fit: nonce-based runtime styles or SSR with nonce
styled-components
Works reasonably well with nonce support, especially in SSR setups.
Best fit: SSR + nonce, or runtime nonce injection
JSS / older Material UI setups
Can be trickier, especially in older versions where style insertion order and nonce handling get weird.
Best fit: test carefully with nonces; consider extraction or framework upgrades
Runtime-heavy design systems
If the system generates lots of one-off styles per interaction, CSP gets harder to keep clean.
Best fit: reconsider whether runtime CSS generation is worth the policy complexity
My recommended decision tree
If you’re deciding what to do, this is the order I’d use:
Choose extracted CSS when:
- you can change the build pipeline
- you want the strongest CSP
- your app does not rely heavily on runtime-generated styles
Choose nonce-based CSS-in-JS when:
- you need runtime styling
- your framework already supports SSR
- your library has solid nonce support
Use unsafe-inline only when:
- you’re in migration
- a third-party dependency forces it
- you’ve documented the exception and planned its removal
A realistic target policy
For a modern app using CSS-in-JS with nonces, I’d aim for something like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic';
style-src 'self' 'nonce-{RANDOM}';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
If you need help understanding strict-dynamic, https://csp-guide.com/strict-dynamic/ is worth reading.
Final take
CSS-in-JS and CSP are compatible, but only if you make deliberate choices.
If your app still needs style-src 'unsafe-inline', be honest about what that means: your CSP is partially strict, not fully strict. That might be fine for now. I’ve shipped that compromise myself. But if you want a policy that holds up under scrutiny, the better paths are nonce-based style tags or build-time CSS extraction.
My bias is simple:
- extracted CSS is best for security
- nonced runtime styles are the practical middle ground
unsafe-inlineis the fallback you should be trying to delete
That’s the tradeoff, and there’s no magic header that removes it.