Content Security Policy Inline Style Blocked? The Fix
Why a CSP with style-src 'self' silently drops every style attribute and <style> block, how to read the console error, and three fixes that keep the policy strict.

If your browser console says the page's settings blocked an inline style, your Content Security Policy (CSP) has a style-src directive that doesn't allow inline CSS. style-src 'self' permits stylesheets loaded from your own origin and nothing else. Every style="..." attribute and every <style> block in the HTML fails that check, so the browser skips them. The page still renders, the user sees no error, and the only trace is a line in the devtools console.
The fix that keeps your policy strict is to move the inline CSS into a stylesheet file. If you can't, you can allow one specific <style> block with a hash or nonce, or allow specific style attributes with 'unsafe-hashes' plus a hash. Adding 'unsafe-inline' makes the message go away too, but it turns the style directive into a formality. I hit this on my own site with nginx sending style-src 'self': every inline style= attribute was blocked silently, which killed the staggered animation delays I'd set per element. I moved those delays into nth-child rules and left the header alone.
What the blocked inline style error actually means#
CSP is a response header. When the browser loads a page with Content-Security-Policy: style-src 'self', it applies that rule to every piece of CSS the page tries to use. A <link rel="stylesheet" href="/app.css"> on the same origin passes. Anything written directly into the HTML doesn't, because inline CSS has no origin to check against.
Chrome reports it 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 ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution.
Firefox uses the wording most people end up searching for:
Content-Security-Policy: The page's settings blocked an inline style (style-src-attr) from being applied because it violates the following directive: "style-src 'self'"
The important part is that a CSP violation isn't a JavaScript exception. Nothing throws, no overlay appears, and no request fails. The browser drops the style and moves on. That's why the breakage looks like a layout bug or an animation bug rather than a security setting. With a staggered animation, every element animates at the same moment instead of one after another, and nothing else on the page looks wrong.
The browser will also send a violation report if the header includes report-to or report-uri. A bare style-src 'self' has neither, so the console is all you get.
style-src-elem vs style-src-attr: which one is complaining#
Newer browsers split style-src into two more specific directives, and the error message tells you which one fired.
| Directive | Controls | Falls back to |
|---|---|---|
style-src-elem |
<style> elements and <link rel="stylesheet"> |
style-src |
style-src-attr |
style="..." attributes on elements |
style-src |
style-src |
Both, when the specific ones aren't set | default-src |
A message that mentions style-src-elem means a <style> block or a stylesheet URL was rejected. A message that mentions style-src-attr means an inline attribute was rejected. If your header only sets style-src 'self', both specific directives inherit from it, which is why one line in nginx blocks both kinds of inline CSS at once.
The split matters when you pick a fix, because the options differ. Nonces only work for <style> elements. Hashes work for elements directly, and for attributes only when 'unsafe-hashes' is also present. MDN's reference for the style-src directive lists the keywords, and the style-src-attr page covers the attribute case.
Fix 1: move the inline CSS into a stylesheet#
This is the fix I used and the one to try first. The policy stays at 'self', nothing needs hashing, and the CSS lives in one file.
The pattern that breaks is a per-element delay written inline:
<li class="card" style="animation-delay: 80ms">First</li>
<li class="card" style="animation-delay: 160ms">Second</li>
<li class="card" style="animation-delay: 240ms">Third</li>
Under style-src 'self' every one of those attributes is ignored. The animation still runs, just with no delay on any item. The same thing happens if you pass an index through a custom property like style="--i: 2" and compute the delay in CSS, because the custom property is still an inline attribute.
The replacement is a set of nth-child rules in the stylesheet:
.card { animation: rise 400ms ease-out both; }
.card:nth-child(1) { animation-delay: 80ms; }
.card:nth-child(2) { animation-delay: 160ms; }
.card:nth-child(3) { animation-delay: 240ms; }
.card:nth-child(4) { animation-delay: 320ms; }
If the list is long, generating the rules beats typing them. This prints a block you can paste into the stylesheet:
count = 8
step_ms = 80
print(".card { animation: rise 400ms ease-out both; }")
for i in range(1, count + 1):
print(f".card:nth-child({i}) {{ animation-delay: {i * step_ms}ms; }}")
print(f".card:nth-child(n+{count + 1}) {{ animation-delay: {count * step_ms}ms; }}")
The last rule gives every item past the generated range the same delay, so a long list doesn't leave late items waiting for seconds. Once the delays live in CSS you can also gate them properly for users who turn motion off; what prefers-reduced-motion does and how to test it covers that, and which CSS animation properties are cheap is worth a look while you're in the file.
The same approach works for most inline styles: give the element a class and put the declaration in the stylesheet. Where the value is truly dynamic, like a user-chosen color or a position from a database, set it from JavaScript through the CSSOM instead.
Setting styles from JavaScript without tripping CSP#
CSP blocks inline style markup, not style changes made through the DOM API. These two lines look similar but behave differently under style-src 'self':
el.style.animationDelay = '160ms'; // allowed
el.setAttribute('style', 'animation-delay: 160ms'); // blocked
Setting properties on element.style goes through the CSSOM and is permitted. Writing the style attribute as a string, whether with setAttribute or by inserting HTML that contains style="...", counts as inline and is blocked. Inserting a <style> element through innerHTML is blocked for the same reason. If a library renders style attributes into its HTML output, that output hits the same wall, so check how it applies styles before assuming it's fine.
Fix 2: allow one style block with a hash or nonce#
Sometimes a small <style> block in the <head> is deliberate, like critical CSS you inline to avoid a render-blocking request. You can allow exactly that block without opening the door to all inline CSS.
A hash is the simplest option for static content. Take the exact text between <style> and </style>, including whitespace, hash it with SHA-256, base64-encode the result, and put it in the directive:
const crypto = require('crypto');
const css = '.hero{color:#222}';
const hash = crypto.createHash('sha256').update(css, 'utf8').digest('base64');
console.log(`style-src 'self' 'sha256-${hash}'`);
Paste the printed directive into your nginx header in place of the plain style-src 'self'. Any edit to the block changes the hash, so this suits CSS that's built once and rarely touched. It breaks the moment someone adjusts a value without regenerating the header, which is an argument for generating the hash in your build step rather than by hand.
A nonce is a random value generated per response, placed both in the header as 'nonce-abc123' and on the element as <style nonce="abc123">. The application has to generate a fresh value on each request and pass it to the template, which a static add_header line in nginx can't do by itself. Use a nonce when the app already renders the HTML and can own the header. Use a hash when the content is static. Neither applies to style="..." attributes.
One more detail: once a nonce or hash appears in style-src, browsers that implement CSP level 2 or later ignore 'unsafe-inline' in that same directive. You can't combine the two to get a soft fallback.
Fix 3: allow specific style attributes with 'unsafe-hashes'#
If you must keep a handful of style="..." attributes and can't change the markup, CSP has a narrow escape hatch. Hash the attribute value, meaning just the text inside the quotes, with the same SHA-256 and base64 steps as above. Then add both the 'unsafe-hashes' keyword and the hash next to 'self' in the style-src directive.
Every distinct attribute value needs its own hash. A staggered list with eight different delays means eight hashes in the header, and the hashes change whenever the delays do. That's why I moved mine into the stylesheet instead of hashing them. 'unsafe-hashes' is meant for a legacy page with a few fixed attributes you genuinely can't reach, not as a general way to keep inline styles.
Why 'unsafe-inline' is the wrong fix for style-src#
The console message itself suggests 'unsafe-inline', and it does make the error disappear:
add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline';" always;
The cost is that any inline CSS from any source is now allowed, including CSS that reaches the page through a markup injection bug. Injected styles are less dangerous than injected scripts, but they aren't harmless. An attacker who can inject CSS can hide or overlay parts of the page, and attribute selectors combined with background images can leak form values to a remote server. Content-security-policy.com has a plain summary of what the unsafe-inline keyword allows.
If you decide to allow all inline styles anyway, be honest that style-src is no longer protecting anything, and keep script-src strict. For a site you control, moving styles into a file is usually mechanical work and worth doing before reaching for the keyword.
What went wrong for me and what I'd do differently#
My nginx config sent style-src 'self'. The inline style= attributes that gave each element its animation delay were blocked, and nothing surfaced except console lines I wasn't looking at. The stagger was simply gone. Moving the delays to nth-child rules fixed it without touching the header.
What I'd do differently is test before enforcing. Browsers support a second header, Content-Security-Policy-Report-Only, that logs violations in the console without blocking anything:
add_header Content-Security-Policy-Report-Only "default-src 'self'; style-src 'self';" always;
Run with that for a while, open the console on your main templates, and you'll see every inline style the real policy would drop. Swap the header name to Content-Security-Policy only when the console is clean.
Two nginx details are worth checking while you're there. The always parameter makes nginx send the header on error responses too, not only on 2xx and 3xx. And add_header directives in a location block replace every add_header from the enclosing server block, so a location that sets its own cache header can silently stop sending your CSP. Both behaviors are documented in the nginx headers module reference.
Confirm what's actually being sent with curl:
curl -sI https://coremesh.online/ | grep -i content-security
If you want a pattern for probing nginx behavior from the command line, the same curl habits I use in testing nginx rate limiting apply here.
A quick grep over your templates tells you how big the cleanup is before you start:
grep -rn --include='*.html' -e 'style=' -e '<style' .
Checklist#
- Read the console message and note whether it names
style-src-attr(attribute) orstyle-src-elem(element). - Grep templates for
style=and<styleso you know what the policy will drop. - Move per-element values like animation delays into
nth-childrules or classes in the stylesheet. - Set dynamic values through
element.style, never throughsetAttribute('style', ...). - For a deliberate inline
<style>block, add a SHA-256 hash tostyle-srcand regenerate it on every change. - Use
'unsafe-hashes'only for a few fixed attributes you can't edit. - Avoid
'unsafe-inline'unless you accept thatstyle-srcthen protects nothing. - Test with
Content-Security-Policy-Report-Onlybefore enforcing. - Check the header with curl, including on a page served by a
locationblock with its ownadd_headerlines.
Quick answers
What does "the page's settings blocked an inline style" mean?
The page's Content Security Policy has a style-src directive, usually style-src 'self', that doesn't allow CSS written directly in the HTML. The browser drops the style attribute or <style> block and logs the message in the console.
What's the difference between style-src-elem and style-src-attr in the error?
style-src-elem governs <style> elements and stylesheet links; style-src-attr governs style="..." attributes. Both fall back to style-src, so a single style-src 'self' blocks both kinds of inline CSS.
Is 'unsafe-inline' safe to add to style-src?
It makes the error go away by allowing all inline CSS, including anything injected through a markup bug. It's the fix of last resort; moving styles to a file, or allowing one block with a hash, keeps the policy useful.
Can I use a nonce for an inline style attribute?
No. Nonces only apply to <style> elements. For a style attribute you need 'unsafe-hashes' plus a hash of the attribute value, or you move the declaration into a stylesheet.
Why do styles set from JavaScript still work under style-src 'self'?
Setting element.style properties goes through the CSSOM, which CSP doesn't treat as inline. Writing the style attribute as a string with setAttribute or innerHTML is treated as inline and gets blocked.
Did this solve it?
One tap. No account.
Sorry about that. What was missing?
from 0 readers
Saved you an hour? Send a tip.
USDC or ETH on Base, Ethereum, Arbitrum and more. Keeps the notes free and the examples running.
Network
0xc783bc5c3fa14cEC3f09A22284bC91C5AF0d59e1
On a computer? Scan the code with your phone wallet, or copy the address.
Pick the network your wallet or exchange uses. USDC works on all six; ETH on Base, Ethereum, Arbitrum and Optimism. Don't send on Tron or Solana: this address doesn't exist there. Tips are optional and unlock nothing; every guide stays free.
Discussion
Start the discussion
Next note