Sass + CSS Modules usually feels boring in the best possible way. You write .module.scss, import it into a component, get locally scoped class names, and move on with your life.
Then CSP shows up and breaks your build in ways that are annoyingly indirect.
The tricky part is that Sass CSS Modules themselves are not the problem. The problem is how your toolchain delivers the compiled CSS to the browser. Some setups emit static .css files. Others inject <style> tags at runtime. CSP treats those very differently.
If you’re building for a developer audience, this distinction matters more than most docs admit.
Mistake #1: Blaming Sass or CSS Modules for a style-src violation
I see this one a lot. Someone enables CSP, imports a Sass module, and gets a console error like this:
Refused to apply inline style because it violates the following Content Security Policy directive:
"style-src 'self'". Either the 'unsafe-inline' keyword, a hash, or a nonce is required.
The immediate reaction is usually: “CSP doesn’t work with Sass modules.”
Nope. CSP works fine with Sass modules. What it blocks is inline style injection.
A typical React, Vite, or webpack dev setup may compile this:
/* Button.module.scss */
.button {
background: rebeccapurple;
color: white;
}
import styles from './Button.module.scss';
export function Button() {
return <button className={styles.button}>Save</button>;
}
Into JavaScript that injects CSS into a <style> element at runtime. That <style> element is what CSP sees. If your policy is:
Content-Security-Policy: default-src 'self'; style-src 'self';
the browser blocks the injected style tag.
Fix
Prefer extracted static CSS files in production.
For webpack, that usually means using mini-css-extract-plugin instead of style-loader:
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.module\.scss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: true,
},
},
'sass-loader',
],
},
],
},
plugins: [new MiniCssExtractPlugin()],
};
Now the browser loads a real stylesheet:
<link rel="stylesheet" href="/assets/app.css">
That works cleanly with:
Content-Security-Policy: style-src 'self';
If you want a deeper refresher on style-src, https://csp-guide.com/style-src/ is a good reference.
Mistake #2: Leaving unsafe-inline in production because dev mode needed it
This is the most common “it works, ship it” CSP mistake.
Real-world example from headertest.com:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-Y2ViYjIwYTUtMTZjYi00MTAzLWJkMzAtYTM4Y2M3YWY5MjI2' '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'
The style-src 'unsafe-inline' part is the giveaway. Sometimes that’s there because a consent manager or tag manager injects styles. Sometimes it’s there because the app’s CSS pipeline injects runtime styles. Either way, it weakens the policy.
For Sass CSS Modules, unsafe-inline is usually a sign that your production build still depends on style injection somewhere.
Fix
Split your CSP between development and production.
Development policy can be looser:
Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval';
Production policy should be tighter:
Content-Security-Policy: default-src 'self'; style-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self';
If a third-party tool really forces inline styles, isolate that decision and document it. Don’t leave unsafe-inline enabled just because your bundler defaults made local development easy.
I’m pretty opinionated on this one: if your production app needs unsafe-inline for your own Sass module pipeline, your build is wrong.
Mistake #3: Confusing inline style="" attributes with stylesheet rules
CSP treats these separately in a way that trips people up.
Your Sass modules compile to stylesheet rules. Fine.
But your component library may also do this:
<div style={{ color: 'red' }}>Alert</div>
Or server-side rendering might output:
<div style="display:none">...</div>
Even if your Sass modules are extracted to external CSS files, those inline style attributes can still trigger CSP violations depending on policy and browser behavior.
Fix
Move presentational styles back into classes wherever possible.
Instead of:
<div style={{ marginTop: '1rem' }}>Content</div>
use:
/* Layout.module.scss */
.spaced {
margin-top: 1rem;
}
import styles from './Layout.module.scss';
<div className={styles.spaced}>Content</div>
This sounds obvious, but I’ve seen teams “fix” CSP for CSS Modules while quietly keeping hundreds of inline style attributes scattered across the component tree.
If you truly need dynamic values, use CSS custom properties carefully:
<div className={styles.box} style={{ '--box-color': color }} />
.box {
background: var(--box-color, gray);
}
That may still run into CSP issues because the style attribute is still inline. So don’t assume custom properties magically bypass policy. Test in the browser against your actual CSP.
Mistake #4: Forgetting that SSR frameworks often inject style tags
Next.js, Remix, and other SSR setups can behave differently between dev and prod, and even differently between routing modes or CSS-in-JS integrations.
Sass modules are usually safe when the framework outputs linked CSS assets. But if you combine Sass modules with a CSS-in-JS library, critical CSS extraction, or custom document rendering, you may end up with server-rendered <style> tags.
That changes your CSP requirements.
Fix
Inspect the final HTML, not just your source code.
If you see this in page output:
<style>.button_ab12c{background:rebeccapurple;color:white}</style>
then style-src 'self' alone won’t allow it. You’ll need one of:
- a nonce on that style tag
- a hash for the exact inline content
unsafe-inlineas the blunt instrument
The best option depends on your framework. If the framework supports CSP nonces for generated styles, use them. Official docs are the right place to start:
For pure Sass CSS Modules, I’d still choose external CSS assets over generated inline <style> blocks every time.
Mistake #5: Adding nonces to scripts and assuming styles are covered too
I’ve reviewed a lot of CSP headers that look like this:
script-src 'self' 'nonce-rAnd0m';
style-src 'self';
Then the team is confused when runtime-injected styles still fail.
Script nonces do not apply to styles. If your app injects <style nonce="...">, that nonce must also be allowed by style-src.
Fix
If you absolutely must allow inline styles with nonces, configure both directives explicitly:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAnd0m';
style-src 'self' 'nonce-rAnd0m';
And ensure your generated style tags actually include the nonce:
<style nonce="rAnd0m">
.button_ab12c { background: rebeccapurple; }
</style>
That said, noncing styles for a Sass CSS Modules pipeline is usually a workaround, not the clean design. External stylesheets are simpler and less fragile.
Mistake #6: Ignoring third-party CSS injection while debugging your module pipeline
You harden your build, extract CSS, remove style-loader, and CSP still reports style-src violations.
At that point the problem may not be your Sass modules at all.
The headertest.com policy is a realistic example. It allows style sources from Google Tag Manager and Cookiebot, and still includes unsafe-inline:
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
That usually means third-party tooling is part of the styling story.
Consent banners, analytics tags, A/B testing tools, and embeds love injecting styles.
Fix
Debug with the browser’s CSP violation messages and isolate who is generating the blocked style.
A practical process:
- Disable third-party tags.
- Reload and test Sass module rendering.
- Re-enable vendors one by one.
- Check the DOM for new
<style>tags or inlinestyle=""attributes.
If your own app works with:
style-src 'self';
but breaks once a consent tool loads, then your Sass pipeline is fine. The CSP exception belongs to the vendor, not your module system.
Be disciplined here. Don’t weaken the whole policy to accommodate one widget without proving that widget is the cause.
Mistake #7: Not testing the production artifact
This one is boring, but it causes endless confusion.
Developers test CSP against the dev server, where HMR injects styles dynamically. Then they assume the same violations will happen in production. Or worse, they never test the actual production build and ship a policy based on guesswork.
Fix
Always test the exact built output.
For example:
npm run build
npm run preview
Then inspect:
- response headers
- generated HTML
- whether CSS is loaded via
<link>or<style> - console CSP errors
- network requests for CSS assets
For a healthy Sass CSS Modules production setup, I want to see something like:
<link rel="stylesheet" href="/assets/index.8f3c2.css">
and a CSP like:
Content-Security-Policy:
default-src 'self';
style-src 'self';
img-src 'self' data: https:;
font-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
That’s boring. Boring is good.
A practical baseline policy
If your app uses Sass CSS Modules compiled into static assets, this is a solid starting point:
Content-Security-Policy:
default-src 'self';
script-src 'self';
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';
Then add narrowly scoped third-party sources only where needed.
For CSP work, I try to keep one rule in mind: write policy for what the browser actually receives, not for what your source files look like.
Your .module.scss files are harmless. The dangerous part is the delivery path. If that path ends in static CSS files from your own origin, CSP is easy. If it ends in injected style tags and scattered inline attributes, CSP gets messy fast.
That’s the real mistake developers make with Sass CSS Modules: they focus on the preprocessor, when they should be auditing the output.