goober is tiny, fast, and refreshingly unpretentious. That’s exactly why people like it. But the moment you try to lock down a real app with Content Security Policy, CSS-in-JS stops being a styling choice and starts becoming a security deployment problem.
The short version: goober usually injects styles into <style> tags at runtime. CSP cares a lot about that. If your policy is strict, those injected styles can get blocked unless you deliberately allow them.
So if you’re using goober and trying to ship a sane CSP, you’re usually choosing between a few patterns:
style-src 'unsafe-inline'style-src 'nonce-...'style-srchashes- avoiding runtime injection for some or all styles
I’ll compare them the way I’d want someone to explain it to me before I wasted a sprint on the wrong setup.
Why goober collides with CSP
goober is a CSS-in-JS library. In the common runtime model, it creates style rules and inserts them into the DOM. From CSP’s point of view, that means inline style behavior is happening, even if you didn’t manually write a style="" attribute.
A typical strict CSP might look something like this real header used by HeaderTest:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-YThmYWY3YmQtZGU3Mi00N2ZhLTllZTktYmYyZWZhMmQxMzJl' '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'
Notice the style-src 'unsafe-inline'. That’s common because third-party widgets and runtime styling often force your hand. But if your app is mostly your own code, you can often do better.
If you want a refresher on the directives themselves, csp-guide.com is useful without being painfully academic.
Option 1: style-src 'unsafe-inline'
This is the easiest way to make goober work.
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAnd0m';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
object-src 'none';
base-uri 'self';
Pros
- Very easy to deploy
- goober works without special plumbing
- Lower risk of breaking production styling
- Useful when you already depend on third-party code that injects styles
Cons
- Weakens CSP significantly for styles
- Allows injected inline styles that a stricter policy would block
- Makes your policy less meaningful if your goal is hardening against XSS impact
My opinion: this is acceptable when you have ugly constraints, especially with tag managers, consent banners, A/B testing tools, or old UI code. I’ve seen plenty of teams pretend they’re shipping “strict CSP” while quietly leaving unsafe-inline on styles forever. That’s not strict. It’s a compromise. Sometimes a justified one, but still a compromise.
For many goober apps, this is the practical default. Not the best one.
Option 2: nonce-based CSP for styles
This is usually the best balance if you want runtime goober injection and a serious CSP.
The idea is simple: generate a nonce per response, include it in the CSP header, and make sure the <style> elements created for goober carry that nonce.
Your header becomes:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{.Nonce}}';
style-src 'self' 'nonce-{{.Nonce}}';
img-src 'self' data: https:;
object-src 'none';
base-uri 'self';
Then in your server:
import crypto from "node:crypto";
import express from "express";
const app = express();
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString("base64");
res.locals.nonce = nonce;
res.setHeader(
"Content-Security-Policy",
[
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}'`,
`style-src 'self' 'nonce-${nonce}'`,
"img-src 'self' data: https:",
"object-src 'none'",
"base-uri 'self'"
].join("; ")
);
next();
});
The tricky bit is getting goober’s injected <style> tag to use that nonce. One pattern is to create or select a target style node yourself:
// server-rendered HTML template
export function Html({ nonce, appHtml }) {
return `
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<style id="goober" nonce="${nonce}"></style>
</head>
<body>
<div id="app">${appHtml}</div>
<script nonce="${nonce}" src="/app.js"></script>
</body>
</html>
`;
}
Then wire goober to use that node:
import { setup } from "goober";
import React from "react";
const target = document.getElementById("goober");
setup(React.createElement, undefined, undefined, target);
Pros
- Much stronger than
unsafe-inline - Works well with SSR setups
- Good fit for apps already generating script nonces
- Keeps runtime styling flexible
Cons
- More plumbing
- Easy to break during SSR/hydration changes
- You need per-request HTML generation, not a fully static shell
- Some teams forget to attach the nonce to every relevant injected style tag
This is the setup I’d choose for most serious goober apps. If you’re already issuing nonces for scripts, extending that discipline to styles is usually worth it.
Option 3: hash-based CSP
Hashes are great for static inline code. They are usually annoying for runtime CSS-in-JS.
Example:
Content-Security-Policy:
default-src 'self';
style-src 'self' 'sha256-AbCdEf123...';
Pros
- Strong policy for known static inline styles
- No per-request nonce generation
- Great for static sites with fixed inline CSS
Cons
- Bad fit for dynamic goober output
- Any style change invalidates the hash
- Painful in apps with conditional rendering or user-driven styling
If your goober styles are generated at runtime, hashes are mostly the wrong tool. You can build around them in constrained cases, but I wouldn’t recommend it unless your “dynamic” styling is actually deterministic and rendered once during build or SSR in a tightly controlled way.
This option makes more sense for a tiny amount of fixed inline critical CSS than for goober’s normal runtime behavior.
Option 4: extract or avoid runtime-injected styles
This is less a CSP trick and more an architectural escape hatch.
If you can move styles into static CSS files, CSP gets dramatically simpler:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{.Nonce}}';
style-src 'self';
img-src 'self' data: https:;
object-src 'none';
base-uri 'self';
Pros
- Cleanest CSP
- Fewer moving parts
- Better long-term maintainability
- Easier debugging when CSP blocks something
Cons
- You lose some of the ergonomics that made goober attractive
- Refactoring can be expensive
- Not all dynamic styling patterns map cleanly to extracted CSS
If your team is using goober mostly for convenience rather than real dynamic styling needs, this is worth considering. I’ve seen apps carry a CSS-in-JS runtime for years when 90% of the styles were static. That’s technical debt wearing a trendy hat.
Comparison table
unsafe-inline
Best for: fast compatibility
Security: weakest
Complexity: low
Operational pain: low
nonce-based
Best for: real apps that want strong CSP and still need goober
Security: strong
Complexity: medium
Operational pain: medium
hash-based
Best for: static inline CSS, not typical goober runtime usage
Security: strong in the right scenario
Complexity: medium to high
Operational pain: high for dynamic styles
extracted static CSS
Best for: teams willing to reduce runtime styling
Security: strongest and simplest
Complexity: high upfront, low later
Operational pain: mostly during migration
My recommendation
If you’re using goober in a modern SSR app, go with nonces.
If you’re stuck with third-party UI junk or a legacy frontend where breakage is unacceptable, start with unsafe-inline, then work backward toward nonces when you have control over style injection.
If your app barely needs dynamic styling, stop fighting the platform and extract more CSS.
I would avoid betting on hashes for goober unless you’ve proven your generated CSS is stable enough to make that practical.
A practical migration path
If your current app is wide open:
- Ship a CSP in report-only mode
- Keep
style-src 'unsafe-inline'initially - Add script nonces first
- Pre-create a nonce-bearing goober style target
- Switch
style-srcfromunsafe-inlineto nonce-based - Watch reports for regressions
That sequence tends to avoid the “security team shipped a header and the app lost all styling” incident.
Final take
goober itself isn’t the problem. Runtime style injection is. CSP just forces you to be honest about what your frontend is doing.
If you want the best blend of security and practicality, use style-src nonces with a dedicated goober style target. If you can’t, admit you’re making a tradeoff and use unsafe-inline deliberately, not accidentally.
That honesty is most of the battle with CSP.