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, andconnect-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-srcstill 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-srcto 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.
Recommended CSP patterns for Chart.js v4
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.