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.com for GTM
  • google-analytics.com for analytics
  • cookiebot.com for consent
  • connect-src expanded for APIs, telemetry, and websockets
  • frame-src for embedded consent UI
  • a nonce plus 'strict-dynamic' for script trust propagation

If you want to inspect your own headers and spot weak points quickly, HeaderTest is handy.

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:

  1. deploy strict Report-Only
  2. click through every page and interaction
  3. add only the blocked origins you actually need
  4. move inline code out of templates
  5. switch to enforcing Content-Security-Policy

Common Bulma CSP breakage

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;

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.