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.
When analytics and consent tools enter the chat
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-srcallows Google Tag Manager, Cookiebot, and Google Analyticsstyle-srcincludes'unsafe-inline', likely because a third-party widget injects styles or requires inline CSSconnect-srcexpands for APIs, telemetry, and WebSocketsframe-srcallows 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-requestsis nice if you’re cleaning up old mixed-content URLs.default-srcis 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:
- Start with
Content-Security-Policy-Report-Only - Load your pages and watch the browser console
- Add only the sources you truly need
- 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.
Recommended CSP templates
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.