Chart.js v4 is one of the easier charting libraries to run under a strict Content Security Policy. That’s the good news.

The less fun part: “easier” does not mean “automatic.” The moment you mix Chart.js with inline bootstrapping code, third-party plugins, CDN delivery, tag managers, or framework hydration tricks, your policy gets messy fast.

I’ve had to clean this up more than once, and the pattern is always the same: the chart library itself is usually fine, but the surrounding app code quietly punches holes in CSP.

Here’s the practical comparison guide for using Chart.js v4 with CSP, with the tradeoffs that actually matter.

The short version

If you self-host Chart.js v4 and initialize charts from non-inline JavaScript, you can usually keep a strong CSP:

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

That’s the ideal.

If you load Chart.js from a CDN, use inline scripts, or depend on plugins that inject styles or evaluate code, you’ll start adding exceptions. Every exception makes the policy less useful.

Why Chart.js v4 is generally CSP-friendly

Chart.js renders to a <canvas>. That matters.

Canvas-based charting avoids a bunch of CSP headaches common with libraries that rely heavily on inline SVG, dynamic style injection, or runtime code generation. Chart.js v4 also doesn’t require eval() in normal usage, which means you typically do not need 'unsafe-eval'.

That’s a big win.

Pros

  • Works well with a strict script-src
  • Usually does not need 'unsafe-eval'
  • Self-hosting is straightforward
  • Canvas rendering avoids many DOM/style CSP issues
  • Easy to pair with nonce- or hash-based app bootstrapping if needed

Cons

  • Inline chart initialization is very common in examples, and that conflicts with strict CSP
  • External images, custom fonts, and remote data endpoints expand img-src, font-src, and connect-src
  • Some plugins may introduce their own CSP requirements
  • CDN usage adds host allowlists you may not need otherwise

Comparison: common ways to deploy Chart.js v4 under CSP

Option 1: Self-host Chart.js and use external app code

This is the cleanest setup.

<script src="/js/chart.umd.min.js"></script>
<script src="/js/dashboard.js"></script>
<canvas id="salesChart"></canvas>
// /js/dashboard.js
const ctx = document.getElementById('salesChart');

new Chart(ctx, {
  type: 'bar',
  data: {
    labels: ['Jan', 'Feb', 'Mar'],
    datasets: [{
      label: 'Sales',
      data: [12, 19, 7]
    }]
  }
});

CSP

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

Pros

  • Strongest CSP
  • No inline script exceptions
  • No third-party script trust
  • Easy to reason about and audit

Cons

  • You need to manage library updates yourself
  • Slightly more build/deploy work
  • If your app fetches chart data from APIs, connect-src still needs to be configured

If you can choose only one approach, pick this one.

Option 2: Self-host Chart.js, but use a nonce for inline chart bootstrapping

Sometimes the page needs server-rendered data and a tiny inline script is the simplest way to initialize the chart.

<script nonce="{{ .Nonce }}">
  const chartData = JSON.parse(document.getElementById('chart-data').textContent);
  new Chart(document.getElementById('salesChart'), {
    type: 'line',
    data: chartData
  });
</script>
<script id="chart-data" type="application/json">
  {"labels":["Jan","Feb"],"datasets":[{"label":"Sales","data":[10,20]}]}
</script>

CSP

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

Pros

  • Still strong if nonces are generated correctly per response
  • Practical for server-rendered pages
  • Avoids 'unsafe-inline'

Cons

  • More implementation complexity
  • Every inline script needs the correct nonce
  • Easy to break in templating systems or cached HTML fragments

Nonce-based policies are solid, but they require discipline. If your rendering pipeline is sloppy, you’ll get random breakage.

For deeper CSP directive details, the script-src behavior is covered well at https://csp-guide.com.

Option 3: Load Chart.js from a CDN

A lot of teams do this first because it’s quick:

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="/js/dashboard.js"></script>

CSP

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.jsdelivr.net;
  style-src 'self';
  img-src 'self' data:;
  connect-src 'self';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';

Pros

  • Fast to set up
  • No need to package the library yourself
  • Fine for prototypes or small sites

Cons

  • Expands script-src to third-party origins
  • Harder to maintain a minimal policy
  • You now trust CDN delivery and availability
  • Teams often end up adding more third-party hosts over time

This is where CSP policies start to drift. One CDN becomes three. Then analytics, then consent tools, then marketing tags.

A real-world header from headertest.com shows what that drift looks like:

content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-YTRlZWNhNDktMDViMi00MzNlLWEzNzctNWYyZTY2ZTg3Mzk2' '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 policy may be justified for that app, but it’s a good reminder: Chart.js is rarely the thing weakening CSP. The surrounding stack is.

Option 4: Inline scripts and 'unsafe-inline'

This is the “it works, ship it” version.

<script>
  new Chart(document.getElementById('salesChart'), {
    type: 'pie',
    data: {
      labels: ['A', 'B'],
      datasets: [{ data: [30, 70] }]
    }
  });
</script>

CSP

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'unsafe-inline';

Pros

  • Easiest setup
  • Matches lots of quick-start examples
  • Useful for throwaway demos

Cons

  • Weakens one of the most valuable CSP protections
  • Makes XSS mitigation much less effective
  • Encourages more inline script sprawl later

I avoid this unless I’m dealing with a temporary internal prototype. For production, I’d rather spend the extra 15 minutes moving code into a file or wiring up a nonce.

Common CSP issues with Chart.js v4

1. Data fetching for charts

If your chart loads data from an API:

fetch('https://api.example.com/stats')
  .then(r => r.json())
  .then(data => {
    new Chart(document.getElementById('chart'), {
      type: 'line',
      data
    });
  });

You need the API in connect-src:

connect-src 'self' https://api.example.com;

Without that, the chart script loads fine, but the data request is blocked.

2. External images in datasets

Some custom plugins or canvas drawing logic use external images. That hits img-src.

const img = new Image();
img.src = 'https://cdn.example.com/logo.png';

CSP needs:

img-src 'self' data: https://cdn.example.com;

If you use base64-encoded images, keep data:.

3. Custom web fonts

Chart labels can rely on custom fonts. If those fonts are loaded remotely, font-src must allow them.

font-src 'self' https://fonts.example.com;
style-src 'self' https://fonts.example.com;

This isn’t a Chart.js problem exactly, but it shows up as “why does my chart text look wrong?”

4. Third-party plugins

Most Chart.js plugins are harmless from a CSP perspective. Some are not. Review them for:

  • Inline style injection
  • Dynamic script loading
  • new Function(...) or eval-like behavior
  • Remote asset fetching

If a plugin forces 'unsafe-eval' or 'unsafe-inline', I treat that as a red flag.

Best security

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

Use this when:

  • Chart.js is self-hosted
  • No inline scripts
  • Data comes from same origin

Balanced and practical

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

Use this when:

  • You need a small inline bootstrap script
  • Data comes from a trusted API
  • You still want a pretty tight policy

What I’d avoid

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.example.com;
  style-src 'self' 'unsafe-inline';

That usually means the app has stopped designing for CSP and started negotiating with every dependency.

My take

Chart.js v4 is a good fit for CSP-conscious apps. Compared to many frontend libraries, it stays out of your way. The strongest setup is boring on purpose: self-host the library, keep initialization out of inline script, and only widen connect-src, img-src, or font-src when you can explain exactly why.

If your CSP around Chart.js is becoming large and ugly, the chart library probably isn’t the real issue. It’s usually analytics, consent tooling, tag managers, or a plugin nobody reviewed closely.

That’s the real comparison to make: not “Can Chart.js work with CSP?” but “Can my whole page stay disciplined enough to let Chart.js work with a strong CSP?”

Usually, yes.

If you keep the rest of the stack honest.