Nivo is usually a pretty easy fit for a strict Content Security Policy. That’s the good news.
The catch is that teams often blame the charting library when the real CSP breakage comes from everything around it: analytics, consent banners, custom fonts, exported images, server-side rendering, or a dev setup that quietly relies on unsafe-eval.
If you’re adding Nivo charts to a production app and want a sane CSP, here’s how I’d approach it.
The short version
For most Nivo charts, you do not need to allow:
unsafe-evalblob:- broad third-party script sources
object-srcanything except'none'
Most Nivo components render with React using SVG or Canvas. That means your CSP usually comes down to:
- where your scripts load from
- whether styles are inline
- whether images inside charts come from remote hosts or
data: - whether fonts are self-hosted or third-party
- whether your app fetches chart data from an API
A solid baseline policy for a self-hosted React app with Nivo often looks like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
style-src 'self';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
That policy is pretty strict, and Nivo itself is usually fine with it.
What Nivo actually needs from CSP
Nivo is a React charting library. In practice, that means chart rendering happens through:
- SVG components like
ResponsiveBar,ResponsiveLine,ResponsivePie - Canvas variants like
ResponsiveLineCanvas,ResponsiveScatterPlotCanvas
From a CSP perspective, those are mostly harmless.
SVG charts
SVG charts are DOM elements. They don’t execute script by themselves. CSP pain points only show up if you:
- inject untrusted HTML into labels or tooltips
- load external images in nodes, markers, or patterns
- rely on inline styles from your app or framework
Canvas charts
Canvas charts draw pixels, not DOM nodes. That usually makes CSP simpler, not harder.
The main edge case is when the chart uses image assets or exports rendered content, which can trigger img-src issues if you pull assets from remote hosts or use data: URLs.
A minimal React example with Nivo
Here’s a simple line chart:
import { ResponsiveLine } from '@nivo/line'
const data = [
{
id: 'sales',
data: [
{ x: 'Jan', y: 120 },
{ x: 'Feb', y: 180 },
{ x: 'Mar', y: 160 },
],
},
]
export default function SalesChart() {
return (
<div style={{ height: 320 }}>
<ResponsiveLine
data={data}
margin={{ top: 20, right: 20, bottom: 50, left: 60 }}
xScale={{ type: 'point' }}
yScale={{ type: 'linear', min: 0, max: 'auto' }}
axisBottom={{ legend: 'Month', legendOffset: 36, legendPosition: 'middle' }}
axisLeft={{ legend: 'Revenue', legendOffset: -40, legendPosition: 'middle' }}
useMesh={true}
/>
</div>
)
}
This chart does not force any unusual CSP directives. If your app scripts, styles, and API requests are already allowed, this works under a strict policy.
Start from a real policy, then trim it
Here’s a real CSP 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-N2I5YzVlYjAtZjc1Mi00OTI5LTkxMDEtN2JiMmMzNzRhM2Ey' '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 realistic production policy because it includes all the usual marketing baggage.
For a Nivo-focused app, I’d split the thinking like this:
Directives Nivo usually cares about
script-srcstyle-srcimg-srcfont-srcconnect-src
Directives that are just good security hygiene
object-src 'none'base-uri 'self'form-action 'self'frame-ancestors 'none'
If you don’t use GTM, Cookiebot, or Google Analytics, don’t copy them into your chart page policy. Teams do this all the time and end up with a giant CSP they don’t understand.
Express example with a nonce-based CSP
If you render your React shell from a Node/Express server, generate a nonce per request and use it for any inline bootstrap script.
import crypto from 'node:crypto'
import express from 'express'
const app = express()
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64')
next()
})
app.use((req, res, next) => {
const nonce = res.locals.nonce
const csp = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
"style-src 'self'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.example.com",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
].join('; ')
res.setHeader('Content-Security-Policy', csp)
next()
})
app.get('/', (req, res) => {
const nonce = res.locals.nonce
res.send(`
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Nivo CSP Demo</title>
<link rel="stylesheet" href="/assets/app.css" />
</head>
<body>
<div id="root"></div>
<script nonce="${nonce}">
window.__APP_CONFIG__ = { apiBase: "https://api.example.com" };
</script>
<script src="/assets/app.js" defer></script>
</body>
</html>
`)
})
app.listen(3000)
If your app is fully static and doesn’t need inline scripts, even better. Drop the nonce and keep script-src 'self'.
The style-src trap with React chart pages
A lot of Nivo examples use inline styles on wrapper elements:
<div style={{ height: 320 }}>
<ResponsiveBar data={data} /* ... */ />
</div>
That matters because CSP treats inline styles differently. If your policy is:
style-src 'self'
then inline style="" attributes may be blocked depending on browser behavior and how your app emits them.
I’ve seen teams fight this for hours and then just add:
style-src 'self' 'unsafe-inline'
That works, but it weakens your policy.
I’d rather move layout styles into CSS classes:
<div className="chartWrap">
<ResponsiveBar data={data} />
</div>
.chartWrap {
height: 320px;
}
Now your CSP can stay tighter:
style-src 'self'
If you need more background on style-src, https://csp-guide.com/style-src/ is a good reference.
Loading chart data from APIs
Most Nivo charts pull data from your own backend or a reporting API. That means connect-src needs to allow those endpoints.
Example:
async function loadChartData() {
const response = await fetch('https://api.example.com/reports/sales')
if (!response.ok) throw new Error('Failed to load chart data')
return response.json()
}
CSP:
connect-src 'self' https://api.example.com
If you use WebSockets for live dashboards:
connect-src 'self' https://api.example.com wss://stream.example.com
That mirrors the real-world pattern in the headertest.com policy, which allows both HTTPS and WSS endpoints.
Images inside Nivo charts
Some chart customizations use images for nodes, markers, or legends. Example:
const data = [
{
id: 'users',
data: [
{ x: 'A', y: 10 },
{ x: 'B', y: 20 },
],
},
]
If you render custom layers or tooltips with avatars:
function CustomTooltip({ point }) {
return (
<div className="tooltip">
<img src={`https://cdn.example.com/avatars/${point.data.x}.png`} alt="" />
<strong>{point.data.xFormatted}</strong>: {point.data.yFormatted}
</div>
)
}
You’ll need:
img-src 'self' data: https://cdn.example.com
I usually keep data: in img-src because apps often end up generating tiny inline assets, placeholders, or exported chart previews that rely on it.
Fonts and Nivo themes
Nivo itself does not need remote fonts. Your design system might.
If you self-host fonts:
font-src 'self'
If your CSS pulls fonts from somewhere else, allow only that host. Don’t broaden font-src to https: unless you really have to.
Server-side rendering and hydration
Nivo apps often live in Next.js or another SSR setup. The chart library itself isn’t the CSP problem. Hydration bootstrapping is.
If your framework emits inline scripts for state transfer, you’ll need nonces or hashes in script-src.
Nonce-based pattern:
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic'
That’s close to the headertest.com policy and it’s a good modern approach. If you want a deeper explanation of how nonces and strict-dynamic work, https://csp-guide.com/strict-dynamic/ is worth reading.
A practical CSP for a Nivo dashboard page
Here’s a policy I’d actually ship for a self-hosted dashboard with Nivo, an API, self-hosted fonts, and no third-party marketing scripts:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self' https://api.example.com wss://stream.example.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
And here’s a more realistic variant if your product team insists on analytics and consent tooling:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
style-src 'self' 'unsafe-inline' https://*.cookiebot.com https://consent.cookiebot.com;
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
frame-src 'self' https://consentcdn.cookiebot.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
I don’t love 'unsafe-inline' in style-src, but consent tools and legacy UI code often force your hand.
Debugging when a Nivo chart “breaks under CSP”
When someone says “CSP broke the chart”, I check these in order:
-
Was the JS bundle blocked?
- Look for
script-srcviolations.
- Look for
-
Did the chart data request fail?
- Check
connect-src.
- Check
-
Are wrapper styles blocked so the chart has zero height?
- This is common and sneaky.
- A responsive Nivo chart inside a container with no height renders like it’s broken.
-
Were tooltip images or custom assets blocked?
- Check
img-src.
- Check
-
Did SSR hydration fail because an inline script lacked a nonce?
- Check
script-srcagain.
- Check
-
Did your dev build rely on
eval?- Some local dev tooling still wants
unsafe-eval. - Don’t carry that into production unless you absolutely must.
- Some local dev tooling still wants
My opinionated default
For Nivo, start strict and loosen only when the browser gives you a concrete violation report.
That usually means:
- self-host scripts and fonts
- avoid inline styles on chart wrappers
- use nonces for any inline bootstrapping
- keep
object-src 'none' - keep
frame-ancestors 'none' - allow only the exact API, image, and analytics hosts you really use
Nivo is not the hard part here. The rest of your frontend stack is. If you keep that mental model, CSP setup for charts gets a lot less mysterious.