Bulma is one of the easier CSS frameworks to secure with Content Security Policy. That’s mostly because Bulma itself is just CSS. No bundled JavaScript, no weird runtime code generation, no framework magic that sneaks in inline scripts behind your back.
That said, real Bulma sites rarely stay “just CSS” for long. You add a navbar burger toggle, a modal, analytics, a consent banner, maybe a form widget, and suddenly your clean CSP turns into a pile of exceptions.
Here’s how I approach CSP for Bulma in production.
Why Bulma is CSP-friendly
Bulma ships as CSS classes. If you self-host the CSS and your own fonts, a very strict baseline works:
Content-Security-Policy:
default-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
script-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
That policy is already pretty solid for a static Bulma site with a tiny external JS file.
A minimal Bulma page that works with this:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Bulma + CSP</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/css/bulma.min.css">
<link rel="stylesheet" href="/css/site.css">
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title">Hello Bulma</h1>
<button class="button is-primary" id="open-modal">Open modal</button>
</div>
</section>
<div class="modal" id="demo-modal">
<div class="modal-background"></div>
<div class="modal-content box">Secure enough.</div>
<button class="modal-close is-large" aria-label="close"></button>
</div>
<script src="/js/app.js"></script>
</body>
</html>
And the JS:
document.addEventListener('DOMContentLoaded', () => {
const modal = document.getElementById('demo-modal');
const open = document.getElementById('open-modal');
const close = modal.querySelector('.modal-close');
const bg = modal.querySelector('.modal-background');
const closeModal = () => modal.classList.remove('is-active');
open.addEventListener('click', () => modal.classList.add('is-active'));
close.addEventListener('click', closeModal);
bg.addEventListener('click', closeModal);
});
No inline script. No inline styles. No drama.
The first place people mess this up
They paste Bulma from a CDN, then add inline JS for the burger menu, then sprinkle in style="display:none" in templates, and finally “fix” everything with:
Content-Security-Policy: default-src * 'unsafe-inline' 'unsafe-eval' data: blob:;
That’s not a CSP. That’s a decorative sticker.
If you want a policy that actually buys you XSS protection, keep these habits:
- self-host Bulma when possible
- move JavaScript into external files
- avoid inline
style="" - avoid event handlers like
onclick= - add third-party sources one by one, not with giant wildcards
A practical Bulma CSP starter policy
For a typical Bulma marketing or docs site, this is a good starting point:
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';
Why img-src https:? Because images are often the first external asset you’ll need, especially from a CMS or avatar service. If you know every image host, lock it down further.
If you want directive-by-directive details, csp-guide.com is a good reference without drowning you in browser trivia.
Using Bulma from a CDN
If you load Bulma from jsDelivr or cdnjs, style-src 'self' will block it.
Example:
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
Then your CSP needs that host:
Content-Security-Policy:
default-src 'self';
style-src 'self' https://cdn.jsdelivr.net;
script-src 'self';
img-src 'self' data: https:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
I still prefer self-hosting for production. Fewer moving parts, fewer CSP exceptions, better control over upgrades.
Inline styles and Bulma helpers
Bulma encourages class-based styling, which helps a lot. Instead of this:
<div class="box" style="margin-top: 2rem;">Content</div>
do this:
<div class="box mt-5">Content</div>
Or define a class in your stylesheet:
.hero-spacer {
margin-top: 2rem;
}
Then:
<div class="box hero-spacer">Content</div>
If your templates or CMS keep injecting inline styles, you’ll be tempted to allow this:
style-src 'self' 'unsafe-inline';
That works, but it weakens CSP. Sometimes you inherit a CMS where this is unavoidable. Fine. Just treat it as technical debt, not a best practice.
Inline scripts for Bulma components: don’t do it
A lot of Bulma examples online use inline script blocks for things like navbar toggles:
<script>
document.addEventListener('DOMContentLoaded', () => {
const burgers = Array.from(document.querySelectorAll('.navbar-burger'));
burgers.forEach(el => {
el.addEventListener('click', () => {
const target = document.getElementById(el.dataset.target);
el.classList.toggle('is-active');
target.classList.toggle('is-active');
});
});
});
</script>
That forces you into one of these:
'unsafe-inline'- a hash
- a nonce
For your own code, the cleanest fix is just moving it into /js/app.js.
document.addEventListener('DOMContentLoaded', () => {
const burgers = [...document.querySelectorAll('.navbar-burger')];
for (const el of burgers) {
el.addEventListener('click', () => {
const target = document.getElementById(el.dataset.target);
el.classList.toggle('is-active');
target?.classList.toggle('is-active');
});
}
});
Then keep:
script-src 'self';
When nonces make sense
Nonces are useful when you genuinely need some inline script, especially with server-rendered pages.
Example HTML:
<script nonce="{{ .CSPNonce }}">
window.appConfig = {
apiBase: "/api",
env: "production"
};
</script>
<script src="/js/app.js" nonce="{{ .CSPNonce }}"></script>
Header:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-r4nd0m123';
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';
Generate a fresh nonce per response. Reusing one across requests defeats the point.
Real-world Bulma sites usually need more than the basics
Here’s a real CSP from headertest.com:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-ODIzZmM0NjYtMGMwNi00MjIxLThiM2QtMzc5OWZlYTY2YTMx' '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 good example of what happens when a simple site grows up:
googletagmanager.comfor GTMgoogle-analytics.comfor analyticscookiebot.comfor consentconnect-srcexpanded for APIs, telemetry, and websocketsframe-srcfor embedded consent UI- a
nonceplus'strict-dynamic'for script trust propagation
If you want to inspect your own headers and spot weak points quickly, HeaderTest is handy.
A Bulma site with analytics and consent
Let’s say your Bulma site uses:
- self-hosted Bulma CSS
- local app JS
- Google Tag Manager
- Cookiebot
- Google Analytics
A realistic policy might look like this:
Content-Security-Policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-{{NONCE}}' '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://*.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';
Would I love to get rid of 'unsafe-inline' in style-src? Yes. In practice, many consent and tag tools make style restrictions annoying enough that teams accept that tradeoff.
Report-Only first, always
Don’t ship a brand-new CSP straight to enforcement on a live Bulma app unless you enjoy surprise outages.
Start with:
Content-Security-Policy-Report-Only:
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';
Then watch the console and violation reports.
A common workflow I use:
- deploy strict
Report-Only - click through every page and interaction
- add only the blocked origins you actually need
- move inline code out of templates
- switch to enforcing
Content-Security-Policy
Common Bulma CSP breakage
Navbar burger doesn’t work
Usually because the toggle code is inline and blocked by script-src.
Fix: move it to an external JS file or use a nonce.
Icons or webfonts fail
You forgot font-src or your icon font is loaded from a CDN.
Fix:
font-src 'self' https://cdn.jsdelivr.net;
Background images disappear
CSS references an external image host not listed in img-src.
Fix:
img-src 'self' data: https://images.example-cdn.com;
Embedded forms or consent popups break
You need frame-src and probably broader connect-src.
My default recommendation
For most Bulma projects, 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';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
Then expand carefully for whatever you actually use.
Bulma is one of the rare frontend choices that makes CSP easier instead of harder. If you keep your JS external and resist the urge to patch over violations with 'unsafe-inline', you can get a pretty strong policy without fighting your CSS framework at all.