Picnic CSS is refreshingly simple. Drop in one stylesheet, get decent defaults, and move on with your life. That simplicity also makes CSP easier than with heavier UI frameworks that drag in fonts, inline scripts, runtime style injection, and mystery third-party assets.

If you’re using Picnic CSS, you can usually get to a pretty strict Content Security Policy without much pain.

What Picnic CSS changes for CSP

Picnic CSS is just CSS. No JavaScript runtime. No client-side style injection. No dependency on external fonts unless you add them yourself.

That means your CSP mostly needs to answer a few questions:

  • Where is the Picnic stylesheet loaded from?
  • Do you use inline styles anywhere?
  • Do you use inline scripts for menus, analytics, or app bootstrapping?
  • Do you load images, fonts, or APIs from other origins?

If Picnic CSS is self-hosted, your CSP can stay very tight.

The ideal setup: self-host Picnic CSS

This is the cleanest option.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Picnic CSS Demo</title>
  <link rel="stylesheet" href="/assets/css/picnic.min.css">
</head>
<body>
  <nav>
    <a href="/" class="brand">My App</a>
    <div class="menu">
      <a href="/docs" class="button">Docs</a>
      <a href="/login" class="button">Login</a>
    </div>
  </nav>

  <main class="container">
    <h1>Hello Picnic</h1>
    <button class="primary">Ship it</button>
  </main>
</body>
</html>

For that page, a strong starting CSP is:

Content-Security-Policy:
  default-src 'self';
  style-src 'self';
  img-src 'self' data:;
  font-src 'self';
  script-src 'self';
  connect-src 'self';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  object-src 'none';

That policy is boring, and boring is exactly what you want.

If you load Picnic CSS from a CDN

A lot of teams start here:

<link rel="stylesheet"
      href="https://cdn.example.com/picnic.min.css">

Then style-src needs that CDN origin.

Content-Security-Policy:
  default-src 'self';
  style-src 'self' https://cdn.example.com;
  img-src 'self' data:;
  font-src 'self';
  script-src 'self';
  connect-src 'self';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  object-src 'none';

If you can self-host, I would. It removes a trust dependency and makes CSP simpler. A CDN is fine, but every extra origin is another thing to audit.

Inline styles are where people weaken CSP

Picnic CSS itself doesn’t require inline styles. Your templates probably do.

This is common:

<div style="margin-top: 2rem;">Welcome back</div>

That will be blocked by:

style-src 'self';

A lot of developers “fix” this by adding 'unsafe-inline' to style-src:

style-src 'self' 'unsafe-inline';

That works, but it weakens CSP. If you care about CSP doing real work, avoid that unless you genuinely need it.

Refactor inline styles into classes instead:

<div class="mt-2">Welcome back</div>
.mt-2 { margin-top: 2rem; }

Then keep:

style-src 'self';

Inline scripts matter more than Picnic CSS

Picnic CSS doesn’t need JavaScript, but your site probably does.

This will break under a strict CSP:

<script>
  document.querySelector('#menu-toggle').addEventListener('click', () => {
    document.body.classList.toggle('nav-open');
  });
</script>

Don’t reach for 'unsafe-inline' in script-src. Use a nonce instead.

HTML with a nonce

<script nonce="{{ .CSPNonce }}">
  document.querySelector('#menu-toggle')?.addEventListener('click', () => {
    document.body.classList.toggle('nav-open');
  });
</script>

Matching CSP header

Content-Security-Policy:
  default-src 'self';
  style-src 'self';
  script-src 'self' 'nonce-r4nd0mBase64Value';
  img-src 'self' data:;
  font-src 'self';
  connect-src 'self';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  object-src 'none';

The nonce must be unique per response.

If you need a refresher on nonce-based CSP, the official spec is the source of truth, and https://csp-guide.com also has good directive-level explanations.

A practical Express example

Here’s a tiny Express app serving Picnic CSS from local files and attaching a nonce-based CSP.

import express from "express";
import crypto from "crypto";
import path from "path";

const app = express();

app.use("/assets", express.static(path.join(process.cwd(), "public/assets")));

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

  const csp = [
    "default-src 'self'",
    "style-src 'self'",
    `script-src 'self' 'nonce-${nonce}'`,
    "img-src 'self' data:",
    "font-src 'self'",
    "connect-src 'self'",
    "base-uri 'self'",
    "form-action 'self'",
    "frame-ancestors 'none'",
    "object-src 'none'"
  ].join("; ");

  res.setHeader("Content-Security-Policy", csp);
  next();
});

app.get("/", (req, res) => {
  res.send(`
    <!doctype html>
    <html>
      <head>
        <meta charset="utf-8">
        <title>Picnic CSP Demo</title>
        <link rel="stylesheet" href="/assets/css/picnic.min.css">
      </head>
      <body>
        <main class="container">
          <h1>Picnic CSS with CSP</h1>
          <button id="toggle" class="primary">Toggle</button>
        </main>

        <script nonce="${res.locals.nonce}">
          document.getElementById('toggle').addEventListener('click', () => {
            document.body.classList.toggle('dark');
          });
        </script>
      </body>
    </html>
  `);
});

app.listen(3000);

That’s a good baseline for real apps.

This is where clean CSPs get messy. A real header from headertest.com looks like this:

content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-NjU1ZTUyODgtYjAyZi00YzcxLWExNmYtODczOWVlNTI5OTVl' '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'

That’s a realistic example of what happens once marketing tooling gets involved:

  • script-src allows Google Tag Manager, Cookiebot, and Google Analytics
  • style-src includes 'unsafe-inline', likely because a third-party widget injects styles or requires inline CSS
  • connect-src expands for APIs, telemetry, and WebSockets
  • frame-src allows consent-related iframes

For a Picnic CSS site, none of that complexity comes from Picnic. It comes from everything around it.

My advice: start with a strict app CSP, then add third-party origins one by one based on actual violations. Don’t paste giant vendor policies into production and call it done.

A stricter production policy for a Picnic app

Here’s a more realistic policy if you self-host assets, use a nonce for inline JS, and avoid inline styles:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
  style-src 'self';
  img-src 'self' data:;
  font-src 'self';
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none';
  upgrade-insecure-requests;

A few notes:

  • 'strict-dynamic' is useful when you trust nonce-bearing scripts to load other scripts. If you don’t have that pattern, you may not need it.
  • upgrade-insecure-requests is nice if you’re cleaning up old mixed-content URLs.
  • default-src is not a substitute for explicit directives. I still prefer declaring the major fetch directives directly.

Common breakages with Picnic CSS pages

1. Inline style="" attributes

Blocked unless you allow inline styles.

2. Tiny inline helper scripts

Stuff like theme toggles, nav toggles, or server-rendered config blobs will fail without a nonce or hash.

3. External icons or fonts

Picnic doesn’t require them, but your design probably does. If you add a hosted font, update font-src and maybe style-src.

4. Data URI images

Some apps use base64 icons or generated image previews. Keep img-src data: if you rely on that.

Debugging strategy that actually works

Use this order:

  1. Start with Content-Security-Policy-Report-Only
  2. Load your pages and watch the browser console
  3. Add only the sources you truly need
  4. Switch to enforcing mode

Example report-only header:

Content-Security-Policy-Report-Only:
  default-src 'self';
  style-src 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}';
  img-src 'self' data:;
  font-src 'self';
  connect-src 'self';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  object-src 'none';

If your team is new to CSP, report-only mode saves a lot of angry debugging.

Minimal Picnic CSS static site

Content-Security-Policy:
  default-src 'self';
  style-src 'self';
  img-src 'self' data:;
  font-src 'self';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  object-src 'none';

Picnic CSS app with JavaScript

Content-Security-Policy:
  default-src 'self';
  style-src 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}';
  img-src 'self' data:;
  font-src 'self';
  connect-src 'self';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  object-src 'none';

Picnic CSS with third-party analytics

Content-Security-Policy:
  default-src 'self';
  style-src 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}' https://www.googletagmanager.com;
  img-src 'self' data: https:;
  connect-src 'self' https://*.google-analytics.com https://*.googletagmanager.com;
  font-src 'self';
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none';

Keep it as small as possible. That’s the whole game.

Picnic CSS is one of the easier front-end libraries to secure with CSP because it doesn’t fight you. If your policy gets bloated, the culprit is probably your own inline code or a third-party script, not the CSS framework. That’s good news, because it means you can usually fix the problem instead of just loosening the policy until everything stops complaining.