Pico.css is refreshingly boring in the best way. Drop in one stylesheet, get decent defaults, move on with your life.
That simplicity tricks people into thinking CSP will be simple too. Usually it is — until someone adds a CDN link, a theme switcher, a bit of inline CSS, or a framework that sneaks in runtime style injection. Then your “tiny CSS setup” turns into a pile of CSP violations.
Here are the mistakes I see most often when teams wire up CSP around Pico.css, and how I’d fix them without gutting the policy.
Mistake 1: Allowing style-src * because “it’s just CSS”
This is the classic lazy fix. Pico.css itself does not need a wide-open style-src. If you self-host it, the policy can be very tight:
Content-Security-Policy:
default-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
script-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
And the HTML is just:
<link rel="stylesheet" href="/css/pico.min.css">
If you load Pico.css from a CDN, allow that specific host and nothing else:
Content-Security-Policy:
default-src 'self';
style-src 'self' https://cdn.jsdelivr.net;
img-src 'self' data:;
font-src 'self';
script-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
>
Don’t use https: as a shortcut unless you genuinely want every HTTPS origin to be allowed for styles. Most people don’t.
Mistake 2: Forgetting that inline styles are blocked
Pico.css works fine without inline styles. Your app usually doesn’t.
This breaks under a strict CSP:
<main style="max-width: 900px; margin: 0 auto;">
...
</main>
If your policy is:
style-src 'self';
the inline style attribute gets blocked.
Better fix: move styles into a stylesheet
.container {
max-width: 900px;
margin: 0 auto;
}
<main class="container">
...
</main>
That’s the cleanest fix and usually the right one.
Sometimes you’ll need 'unsafe-inline'
I don’t love it, but sometimes teams have server-rendered templates full of inline style attributes and no appetite for cleanup. Then the practical policy becomes:
style-src 'self' 'unsafe-inline';
This is common in the wild. For example, the real CSP on headertest.com includes:
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
That’s not there because Pico.css needs it. It’s there because real sites often have consent banners, analytics tooling, or legacy markup that injects or relies on inline styles.
If you want the exact semantics of style-src, csp-guide.com has a good directive reference.
Mistake 3: Using a CDN stylesheet but forgetting fonts or images
Pico.css itself is usually just CSS. Your page may still reference local icons, data URIs, Open Graph previews, user-uploaded images, or custom fonts.
A too-minimal policy often looks like this:
Content-Security-Policy:
default-src 'self';
style-src 'self' https://cdn.jsdelivr.net;
Then you wonder why images vanish or fonts fail.
A more realistic baseline:
Content-Security-Policy:
default-src 'self';
style-src 'self' https://cdn.jsdelivr.net;
img-src 'self' data: https:;
font-src 'self' https:;
script-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
I’m okay with img-src https: more often than I am with style-src https:. Images are less sensitive than script and style, and lots of sites legitimately pull them from multiple origins.
For fonts, be more specific if you can:
font-src 'self' https://fonts.gstatic.com;
style-src 'self' https://cdn.jsdelivr.net https://fonts.googleapis.com;
If you don’t use remote fonts, keep font-src 'self'.
Mistake 4: Reaching for default-src and assuming it covers everything
A lot of developers write this and call it done:
Content-Security-Policy:
default-src 'self';
That is a start, not a finished policy.
Yes, default-src acts as a fallback for several fetch directives. But once you have real pages, you usually need explicit directives for style-src, img-src, font-src, connect-src, and script-src.
For a self-hosted Pico.css site with minimal JavaScript, I’d rather see this:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
That policy is readable. You can tell what the app is allowed to do. When something breaks, the fix is obvious.
Mistake 5: Copy-pasting a CSP from a JavaScript-heavy app
I see this a lot with small marketing sites using Pico.css. Someone grabs a policy full of nonces, strict-dynamic, analytics hosts, websocket origins, and third-party widgets. Now the CSP technically works, but nobody on the team understands it.
Here’s a real header from headertest.com:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-MjI0NDEzMGMtNTNmMi00ZmI1LWE4YTEtYzdjZTM0Mzc1Zjc5' '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 sensible policy for that site. It has analytics, Cookiebot, API calls, and websocket traffic. It is not a sensible starting point for a static docs page using Pico.css.
Don’t inherit complexity you don’t need.
Mistake 6: Allowing inline scripts because you confused script problems with style problems
Pico.css is CSS. It does not need script-src 'unsafe-inline'.
If your page has a tiny bit of JavaScript, use a nonce instead of opening the door to inline script execution.
Server
import crypto from "node:crypto";
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString("base64");
res.setHeader(
"Content-Security-Policy",
[
"default-src 'self'",
`script-src 'self' 'nonce-${res.locals.nonce}'`,
"style-src 'self'",
"img-src 'self' data:",
"font-src 'self'",
"object-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'"
].join("; ")
);
next();
});
Template
<script nonce="{{nonce}}">
document.documentElement.dataset.theme =
localStorage.getItem("theme") || "light";
</script>
That gives you the small inline bootstrap script people often want for theme handling, without falling back to 'unsafe-inline'.
Mistake 7: Breaking theme toggles by injecting styles at runtime
A lot of Pico.css sites add dark mode or custom theming later. Then somebody does this:
const style = document.createElement("style");
style.textContent = `
:root {
--pico-primary: #7c3aed;
}
`;
document.head.appendChild(style);
Under style-src 'self', that dynamic style block is blocked unless you weaken the policy.
My fix: don’t inject CSS text from JavaScript for simple theming. Toggle classes or data attributes instead.
:root[data-brand="violet"] {
--pico-primary: #7c3aed;
}
document.documentElement.setAttribute("data-brand", "violet");
That keeps CSP tight and your theming logic boring.
If your frontend stack insists on runtime style injection, you’ll need to account for that. At that point this is no longer really a Pico.css problem — it’s a tooling problem.
Mistake 8: Shipping CSP without Report-Only first
CSS-related CSP mistakes are easy to miss because the page may still mostly render. One missing source can quietly break a consent banner, a font, or a theme override.
Roll out in report-only mode first:
Content-Security-Policy-Report-Only:
default-src 'self';
style-src 'self' https://cdn.jsdelivr.net;
img-src 'self' data: https:;
font-src 'self';
script-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
Then watch violations in DevTools and your reporting pipeline.
If you’re using a reporting endpoint, add report-to or report-uri depending on your setup. I’d still manually test key pages because browser reports can be noisy and incomplete.
A sane starting policy for Pico.css
If I were starting a normal Pico.css site today, self-hosted CSS, minimal JavaScript, no weird runtime styling, I’d start here:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
If you use the jsDelivr CDN for Pico.css:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' https://cdn.jsdelivr.net;
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
That’s enough for a lot of sites.
The big idea is simple: Pico.css should let you keep CSP boring. If your policy is getting messy, the problem usually isn’t Pico.css. It’s the extra stuff layered on top — inline styles, injected CSS, third-party widgets, consent tooling, analytics, or copied-and-pasted headers nobody pruned. Keep the stylesheet boring, keep the policy explicit, and your CSP will be much easier to live with.