Fix CSP-Blocked Inline Styles with style-src

This guide is part of the Content-Security-Policy (CSP) reference and covers the exact directives that govern CSS: style-src, style-src-elem, and style-src-attr. When a policy without 'unsafe-inline' reaches the browser, every <style> block, every style="…" attribute, and every stylesheet inserted at runtime by a framework is checked against style-src. A block that does not match is dropped, the element renders unstyled, and the console logs a Refused to apply inline style violation. This page shows the precise values that unblock each case, how to authorize CSS by nonce or hash instead of the wildcard 'unsafe-inline', and how to keep CSS-in-JS runtimes working under a strict policy.

Configuration Syntax & Exact Values

CSS enforcement is split across three directives. style-src is the parent; style-src-elem governs stylesheet elements (<style> blocks and <link rel="stylesheet">), and style-src-attr governs inline style attributes. When the two granular directives are absent, the browser falls back to style-src for both cases.

Content-Security-Policy: default-src 'self'; style-src 'self' 'nonce-r4nd0m'; style-src-attr 'none'; img-src 'self'; object-src 'none'; base-uri 'self'

Annotated breakdown of the CSS-relevant tokens:

The critical asymmetry: a <style> element can be authorized with a nonce or a hash, but a style attribute cannot be authorized with a plain nonce or hash — it requires either 'unsafe-inline' or the 'unsafe-hashes' keyword paired with the attribute’s hash. This is why style-src-attr exists as a separate lever.

How the browser routes a CSS check to a style directive A decision tree showing that stylesheet elements resolve against style-src-elem or style-src while inline style attributes resolve against style-src-attr or style-src, and how each is authorized. CSS to apply what kind? <style> / <link> element checked by style-src-elem style="..." attribute checked by style-src-attr Allowed by: 'nonce-...' · 'sha256-...' 'self' (for <link>) · 'unsafe-inline' Allowed by: 'unsafe-inline' 'unsafe-hashes' + 'sha256-...' Missing granular directive falls back to style-src A nonce or hash never authorizes a style attribute
Element styles and attribute styles are checked by different directives, and only element styles can be authorized by nonce or hash.

Nonce for <style> elements

A nonce is a per-response random token echoed into both the header and the matching element. Because the same value must appear in the served HTML, mint it per request — see how to generate per-request CSP nonces.

Content-Security-Policy: default-src 'self'; style-src 'self' 'nonce-Xy7Kp2Qm'
<style nonce="Xy7Kp2Qm">
  .banner { background: #0b5; color: #fff; }
</style>

Hash for a static <style> element

When there is no per-request render step, hash the exact bytes between the <style> tags — identical to the hash-based approach for inline scripts, but the digest lands in style-src.

# Digest of exactly the CSS text, no surrounding tags, no trailing newline
printf "%s" ".banner{background:#0b5;color:#fff}" | openssl dgst -sha256 -binary | openssl base64
# -> uses the resulting value below
Content-Security-Policy: default-src 'self'; style-src 'self' 'sha256-Ck9mVj0lYt0V3q1r0Z0e2r4kQ0Zx8b5tPqK1uY3hE9A='

Server-Side Configuration

Every block below emits the same policy at the edge or in the application. The always / equivalent flag is mandatory: without it the header is attached only to 2xx/3xx responses, so a 304 Not Modified or a 500 error page ships without CSP and browsers render those responses under no policy at all.

Nginx

add_header inside a location or server block replaces inherited headers if any child block adds its own, so keep the CSP declaration at one level. always forces the header onto every status code.

# always -> attach on 304/4xx/5xx too, not just 2xx/3xx
add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'nonce-$request_id'; style-src-attr 'none'; object-src 'none'; base-uri 'self'" always;

For deeper Nginx patterns see the Nginx security headers guide. Note that $request_id is a convenient per-request token but must match the nonce injected into the HTML by your application or SSI layer.

Apache

Header always set writes the header on all responses; set alone skips internal error documents. Place it in the VirtualHost or an .htaccess file.

# always -> covers ErrorDocument and 304 responses
Header always set Content-Security-Policy "default-src 'self'; style-src 'self' 'sha256-Ck9mVj0lYt0V3q1r0Z0e2r4kQ0Zx8b5tPqK1uY3hE9A='; style-src-attr 'none'; object-src 'none'; base-uri 'self'"

The Apache hardening guide covers mod_headers ordering and .htaccess inheritance.

Node / Express (Helmet)

Helmet’s contentSecurityPolicy middleware sets the header on every response it processes. Pass a function to inject a per-request nonce generated upstream.

const helmet = require("helmet");
const crypto = require("crypto");

app.use((req, res, next) => {
  res.locals.cspNonce = crypto.randomBytes(16).toString("base64");
  next();
});

app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],
      // Helmet emits the directive on all responses it handles
      styleSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`],
      styleSrcAttr: ["'none'"],
      objectSrc: ["'none'"],
      baseUri: ["'self'"],
    },
  })
);

Render the nonce into any inline <style nonce="…">. The full setup is in the Helmet configuration guide.

Django / FastAPI

A middleware attaches the header to every response object, including error responses, so no always equivalent is needed — the object always carries it.

class CSPMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)
        # Set on the response object -> present on all status codes
        response["Content-Security-Policy"] = (
            "default-src 'self'; "
            "style-src 'self' 'nonce-%s'; "
            "style-src-attr 'none'; object-src 'none'; base-uri 'self'"
        ) % request.csp_nonce
        return response

See the Django and FastAPI middleware guide for wiring the nonce into templates.

Next.js / Vercel

Static header rules in next.config.js cannot mint a per-request nonce, so use hashes there or set a nonce in middleware. headers() applies the header to matched routes on every response.

// next.config.js -- static, hash-based (no per-request value possible here)
module.exports = {
  async headers() {
    return [
      {
        source: "/:path*",
        headers: [
          {
            key: "Content-Security-Policy",
            value:
              "default-src 'self'; style-src 'self' 'sha256-Ck9mVj0lYt0V3q1r0Z0e2r4kQ0Zx8b5tPqK1uY3hE9A='; style-src-attr 'none'; object-src 'none'; base-uri 'self'",
          },
        ],
      },
    ];
  },
};

The Next.js header management guide shows the middleware nonce pattern. On Cloudflare, a Transform Rule or Worker can inject the same header at the edge.

Diagnostic & Verification Steps

Confirm the header is present and correct before touching markup. curl -I prints response headers only:

curl -sI https://example.com/ | grep -i content-security-policy

Expected output — the CSP line with your style directives intact:

content-security-policy: default-src 'self'; style-src 'self' 'nonce-Xy7Kp2Qm'; style-src-attr 'none'; object-src 'none'; base-uri 'self'

To read the header on a non-2xx response and prove the always flag works, request a path that 404s:

curl -sI https://example.com/does-not-exist | grep -iE 'HTTP/|content-security-policy'
# HTTP/2 404
# content-security-policy: default-src 'self'; style-src 'self' ...

If the second line is missing on the 404, the always flag (or its framework equivalent) is not applied.

Browser evaluation of an inline style block against a nonce policy A sequence showing the server sending a CSP header and matching nonce, the browser comparing the element nonce, and the style applying only when the values match. Server Browser CSP: style-src 'nonce-Xy7Kp2Qm' HTML: <style nonce="Xy7Kp2Qm"> compare element nonce to policy nonce match -> style applied mismatch/absent -> Refused to apply inline style
The browser applies the block only when the element's nonce equals the nonce in the served CSP header.

In DevTools, an unauthorized style logs a precise message in the Console:

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.

When the message names a style attribute, DevTools reports the violated directive as style-src-attr (or style-src on fallback). Run the policy in Report-Only first and read the document.styleSheets count against the visual result: if a block is silently dropped, its rules never appear in document.styleSheets.

// Count applied stylesheets after load -- a dropped <style> is absent here
console.log(document.styleSheets.length);

Edge Cases, Security Implications & Safe Rollback

Trap 1 — CSS-in-JS runtime injection. Libraries such as styled-components, Emotion, and MUI insert a <style> element into <head> at runtime and write rules into it via the CSSOM. Under a nonce policy, these libraries must be told the nonce so the injected element carries it. styled-components and Emotion read a __webpack_nonce__ global; MUI accepts a nonce on its cache/StyleSheetManager.

// Set before the CSS-in-JS runtime creates its <style> element
// styled-components / Emotion read this global to nonce injected styles
__webpack_nonce__ = "Xy7Kp2Qm"; // must equal the CSP nonce for this response

If the library instead mutates an existing sheet through insertRule, that path is not re-checked by CSP — the sheet was authorized once at insertion. The failure mode is the initial <style> element being blocked, not individual rules.

Authorization matrix for inline CSS under a strict policy A matrix comparing which CSP keywords authorize style elements versus style attributes versus runtime injected style elements. CSS case nonce hash 'unsafe-inline' <style> block yes yes yes style="..." attr no only w/ 'unsafe-hashes' yes runtime <style> yes, if injected no yes A nonce on a runtime block requires the library to stamp the nonce onto the element it creates
Only element styles take a nonce or hash; attribute styles need 'unsafe-inline' or 'unsafe-hashes'.

Trap 2 — style attributes you cannot remove. Third-party widgets, sanitized rich text, and some UI libraries write style="…" attributes directly onto elements. A nonce will not help. The clean fix is to refactor those into classed rules under a nonced or 'self' stylesheet. If refactoring is impossible, 'unsafe-hashes' 'sha256-…' (the hash of the attribute’s declaration string) authorizes specific attribute values without opening the blanket 'unsafe-inline' — but every distinct attribute value needs its own hash, and 'unsafe-hashes' widens the attack surface, so scope it to style-src-attr only.

Trap 3 — nonce leaks via CSS injection. 'unsafe-inline' on styles is not harmless: attacker-injected CSS can exfiltrate data through attribute selectors and background-image requests, and can overlay clickjacking surfaces. Removing 'unsafe-inline' from CSS is a real security gain, not cosmetic. Note that when both 'unsafe-inline' and a nonce/hash appear in the same directive, browsers that support nonces ignore 'unsafe-inline' — a useful backward-compatibility bridge for old browsers.

Rollback. Ship the policy as Content-Security-Policy-Report-Only first so violations are reported but nothing is blocked; styles keep rendering while you collect reports. To revert instantly in production, keep the previous working header value ready and swap it back at the edge — an Nginx/Apache/Cloudflare header change deploys without touching application code.

Content-Security-Policy-Report-Only: default-src 'self'; style-src 'self' 'nonce-Xy7Kp2Qm'; style-src-attr 'none'; report-uri /csp-report

Conclusion

Land this change in stages: enforce the new style-src in staging until the console is clean, then ship it to production as Content-Security-Policy-Report-Only (optionally alongside a short-lived enforcing policy on a canary) to confirm real-traffic reports show no legitimate style being dropped, and only then promote it to the enforcing Content-Security-Policy header. Keeping the prior header value one edit away means any regression is a single-line rollback.

Frequently Asked Questions

Why does my style="…" attribute still get blocked after I added a nonce to style-src? Nonces authorize stylesheet elements, never style attributes. Attribute styles are governed by style-src-attr, which accepts only 'unsafe-inline' or 'unsafe-hashes' with a matching hash. Move the declarations into a nonced <style> block or a classed rule to fix it properly.

What is the difference between style-src-elem and style-src-attr? style-src-elem controls <style> blocks and <link rel="stylesheet"> elements; style-src-attr controls inline style="…" attributes. When either granular directive is absent, the browser falls back to style-src for that case. Splitting them lets you allow nonced stylesheets while banning all attribute styles with style-src-attr 'none'.

Can I use a hash for an inline style attribute? Only with the 'unsafe-hashes' keyword. Add 'unsafe-hashes' plus the 'sha256-…' digest of the attribute’s exact declaration string to style-src-attr. Each distinct attribute value needs its own hash, and 'unsafe-hashes' broadens exposure, so prefer refactoring attributes into stylesheet rules.

My styled-components / Emotion styles break under CSP — how do I fix them? These libraries inject a <style> element at runtime, which a nonce policy blocks unless the element carries the nonce. Set __webpack_nonce__ to the current request’s nonce before the runtime creates its sheet, or pass the nonce through the library’s cache/StyleSheetManager. The value must equal the nonce in that response’s CSP header.

Does adding 'unsafe-inline' alongside a nonce weaken the policy? No. Browsers that understand nonces ignore 'unsafe-inline' when a nonce or hash is present in the same directive, so modern browsers enforce the nonce while legacy browsers fall back to 'unsafe-inline'. It is a safe compatibility bridge, not a downgrade for supporting browsers.