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:

  1. style-src 'unsafe-inline'
  2. style-src 'nonce-...'
  3. style-src hashes
  4. 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:

  1. Ship a CSP in report-only mode
  2. Keep style-src 'unsafe-inline' initially
  3. Add script nonces first
  4. Pre-create a nonce-bearing goober style target
  5. Switch style-src from unsafe-inline to nonce-based
  6. 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.