Vanilla Extract is one of the easier styling tools to live with under a strict Content Security Policy. That’s the good news.
The reason is simple: Vanilla Extract compiles styles to real CSS files at build time. No runtime style injection, no CSS-in-JS engine pushing <style> tags into the DOM on page load, no constant fight with style-src nonces. If you care about CSP, that’s already a huge win.
Still, “works better with CSP” is not the same thing as “done.” Teams often ship a decent script-src and then quietly leave style-src 'unsafe-inline' hanging around forever because some analytics tool, consent manager, or framework edge case made it convenient.
I’ve seen that exact pattern in production. The real header from headertest.com is a good example:
content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-OGMwZTY0ODMtMjBiNy00ZjE5LWFlMmEtZWNiNTVmNjZkMmIw' '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’s a pretty normal modern CSP: decent script controls, explicit third-party domains, but style-src 'unsafe-inline' still present. If your app uses Vanilla Extract, you can usually do better than that.
Why Vanilla Extract fits CSP better than most styling approaches
Vanilla Extract writes CSS at build time. That changes the CSP conversation completely.
With runtime CSS-in-JS, you often need one of these:
style-src 'unsafe-inline'- a nonce on injected
<style>tags - a hash-based policy if the inline CSS is stable enough
With Vanilla Extract, most styling ends up in static .css assets served from your origin. That means a simple policy often works:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-...';
style-src 'self';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
That’s the ideal setup for Vanilla Extract: external stylesheets only, same-origin, no inline styles allowed.
The main CSP options for Vanilla Extract
There are really three practical approaches.
Option 1: style-src 'self' only
This is the cleanest option and usually the best one.
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-random123';
style-src 'self';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
Pros
- Strong CSP posture
- No inline CSS allowed
- Easy to reason about
- Perfect fit for Vanilla Extract’s build-time CSS output
- Fewer moving parts than nonce-based style policies
Cons
- Breaks if anything injects inline
<style>tags - Breaks if some component library sneaks in runtime styling
- Third-party widgets often force exceptions
If your stack is mostly Vanilla Extract plus standard external CSS files, this is the one I’d choose first. It’s the least annoying to maintain and the most honest about what your app actually needs.
Option 2: style-src 'self' 'nonce-...'
This is the compromise option. You still want a strict CSP, but you have a few inline styles or framework-generated <style> blocks that need to survive.
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-random123' 'strict-dynamic';
style-src 'self' 'nonce-random123';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
And then in HTML:
<style nonce="random123">
.critical-shell {
display: grid;
}
</style>
Pros
- Much better than
unsafe-inline - Allows controlled inline styles
- Useful if your framework emits critical CSS inline
- Works well when nonces are already part of your
script-src
Cons
- More operational complexity
- Every response needs a fresh nonce
- Easy to get wrong in SSR, streaming, or cached HTML
- Vanilla Extract usually doesn’t need this, so it can feel like paying complexity tax for other tools
This option makes sense when Vanilla Extract is only part of the picture. Maybe the app shell uses static CSS, but your framework still injects some critical CSS or a third-party component writes inline <style> tags.
If you don’t actually need inline styles, don’t add a nonce just because it feels modern.
For directive behavior and nonce rules, the official CSP docs and deeper directive explainers are worth checking, and csp-guide.com is useful when you want quick examples.
Option 3: style-src 'unsafe-inline'
This is the “make it work today” option. It’s common, and I don’t love it.
The headertest.com policy uses it:
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
Pros
- Lowest friction
- Third-party tools usually just work
- No nonce plumbing required
- Easy drop-in for mixed stacks
Cons
- Weakens CSP significantly for styles
- Allows arbitrary inline CSS
- Makes policy drift more likely
- Hides architecture problems you should probably fix
To be fair, inline CSS is generally less catastrophic than inline script. But “less bad” is not “good.” If your app uses Vanilla Extract, unsafe-inline is often a sign that some other dependency is dragging your policy down.
A practical comparison
Here’s the short version.
| Approach | Security | Complexity | Vanilla Extract fit | Third-party compatibility |
|---|---|---|---|---|
style-src 'self' |
Strong | Low | Excellent | Medium |
style-src 'self' 'nonce-...' |
Strong | Medium/High | Good | Good |
style-src 'self' 'unsafe-inline' |
Weakest | Low | Usually unnecessary | Excellent |
My bias: start with style-src 'self', then add nonces only if something truly requires inline styles. Treat unsafe-inline as a last resort.
What usually breaks a strict style policy
Vanilla Extract itself is rarely the problem. The usual offenders are:
- consent banners
- analytics tag managers
- A/B testing tools
- component libraries with runtime style injection
- framework features that inline critical CSS
- inline
style=""attributes in templates
That last one matters more than people think. Even if your stylesheet story is clean, random markup like this will fail under a strict style policy:
<div style="margin-top: 12px"></div>
Vanilla Extract can help you avoid that entirely:
// styles.css.ts
import { style } from '@vanilla-extract/css';
export const spaced = style({
marginTop: '12px',
});
import * as styles from './styles.css';
export function Box() {
return <div className={styles.spaced} />;
}
That’s not just cleaner architecture. It also keeps your CSP simpler.
Recommended CSP for a Vanilla Extract app
If you control your frontend and don’t depend on style-injecting libraries, I’d start here:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
style-src 'self';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
A few notes:
style-src 'self'is usually enough for Vanilla Extract- keep nonces on scripts if you need inline bootstrapping
- only add third-party origins where they are actually needed
- don’t copy giant CSPs between projects without trimming them
If you have a consent platform or tag manager that forces style exceptions, isolate that decision. Don’t assume your app styling needs unsafe-inline just because one vendor does.
When a nonce for styles is actually worth it
I’d use a style nonce if all of these are true:
- your app is mostly strict already
- a small amount of inline style is unavoidable
- you can reliably generate and attach a nonce per response
- you want to avoid
unsafe-inline
That looks like this in server code:
import crypto from 'node:crypto';
export function buildCsp() {
const nonce = crypto.randomBytes(16).toString('base64');
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'`,
].join('; ');
return { nonce, csp };
}
Then pass nonce into your HTML renderer for any allowed inline blocks.
My take
Vanilla Extract is one of the rare frontend styling tools that actually makes CSP easier instead of harder. That alone is a strong reason to like it.
The best CSP pairing for Vanilla Extract is usually boring:
- external CSS
style-src 'self'- no inline styles
- no
unsafe-inline
If your current policy looks like the headertest.com example and you’re using Vanilla Extract, I’d question whether style-src 'unsafe-inline' is really there for Vanilla Extract at all. Most likely it’s there for third-party tooling or legacy markup. That’s useful to know, because it tells you where to focus cleanup work.
Don’t overcomplicate the style policy if your CSS is already static. Vanilla Extract earned you that simplicity. Use it.