Skip to main content

Content Security Policy (CSP)

Unovis supports strict CSP setups that require a per-request nonce on every injected <style> and <script> element. Integration is opt-in and consists of one line — assigning window.UNOVIS_NONCE before the library is imported. Consumers who don't use CSP need to do nothing; the library remains fully backward-compatible.

Why a nonce?

Every Unovis component ships its styles via Emotion, which injects <style> elements into document.head at runtime. Under a strict style-src directive those tags are rejected unless they carry either 'unsafe-inline' (defeats the point of CSP), a matching hash, or the request's nonce. Unovis hands its Emotion cache the nonce you provide, so every one of its <style> tags satisfies style-src 'self' 'nonce-<value>'.

Passing a nonce requires Unovis to create its own Emotion cache instance rather than relying on the default @emotion/css singleton, and Emotion requires every cache sharing a page to use a distinct key — reusing the default css key risks two caches "fighting" over the same style elements. Because of this, when UNOVIS_NONCE is set, Unovis's generated class names carry a unovis- prefix (e.g. unovis-1a2b3c) instead of the usual css- prefix. Consumers who don't set UNOVIS_NONCE are unaffected — Unovis keeps using the default @emotion/css singleton and its css- prefix. Either way, this only affects the auto-generated, content-hashed class names Emotion assigns internally — the supported styling API (the --vis-* CSS custom properties documented in Theming) is unaffected.

Quick start — set UNOVIS_NONCE

Set window.UNOVIS_NONCE to the value your server issued for the current request, before any @unovis/* module is evaluated. The safest place is an inline <script> at the very top of <head> — same nonce as the CSP header:

<!doctype html>
<html>
<head>
<script nonce="<SERVER_NONCE>">window.UNOVIS_NONCE = "<SERVER_NONCE>"</script>
<!-- your framework bundle imports @unovis/* below this line -->
<script nonce="<SERVER_NONCE>" type="module" src="/app.js"></script>
</head>
<body>...</body>
</html>

That's it. Every <style> Emotion injects for Unovis will now carry nonce="<SERVER_NONCE>" and be accepted by the browser.

Timing matters

The nonce is captured once, when the @unovis/ts Emotion module first evaluates. Setting window.UNOVIS_NONCE after the library has loaded has no effect on the styles it has already injected.

A minimum-friction, nonce-based policy that works with every Unovis chart looks like this:

Content-Security-Policy:
default-src 'self';
script-src-elem 'self' 'nonce-<SERVER_NONCE>';
script-src-attr 'none';
style-src-elem 'self' 'nonce-<SERVER_NONCE>';
style-src-attr 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data: https:;
connect-src 'self' https:;
worker-src 'self' blob:;

Notes on each directive:

  • script-src-elem 'nonce-…' — enforces the nonce on <script> blocks and external src scripts.
  • script-src-attr 'none' — blocks inline event-handler attributes like onclick="…"; Unovis never emits any.
  • style-src-elem 'nonce-…' — enforces the nonce on <style> blocks and <link rel="stylesheet"> elements. This is the one that protects Unovis's Emotion output.
  • style-src-attr 'unsafe-inline'required. Nonces cannot cover inline style="…" attributes (CSP3 limitation), and d3-selection, Leaflet, and other DOM libraries Unovis depends on set them on every update. There is no way around this short of a per-page hash allowlist that changes with every data update.
  • img-src 'self' data: blob: — allows tile images, generated SVG thumbnails, and inline data URIs.
  • font-src 'self' data: https: — only required if you use graph node icons; Unovis loads Font Awesome from cdnjs.cloudflare.com for the built-in icon set. Self-host the font to tighten this to 'self' data:.
  • worker-src 'self' blob: — only required if you render the ELK-layered graph, which spawns a Web Worker from a Blob URL.

Framework integration

Below are copy-paste snippets for wiring the nonce into each supported framework. The pattern is always the same:

  1. Have your server emit a nonce per request and echo it into both the Content-Security-Policy header and every <script nonce> / <style nonce> tag it renders.
  2. Set window.UNOVIS_NONCE in an inline nonced <script> before your app bundle loads.

Vite / plain HTML entry — edit index.html:

<script nonce="<%= nonce %>">window.UNOVIS_NONCE = "<%= nonce %>"</script>
<script nonce="<%= nonce %>" type="module" src="/src/main.tsx"></script>

Next.js App Router — set the nonce in middleware.ts, then read it in the root layout:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware (req: NextRequest): NextResponse {
const nonce = crypto.randomUUID().replace(/-/g, '')
const csp = [
"default-src 'self'",
`script-src-elem 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src-elem 'self' 'nonce-${nonce}'`,
"style-src-attr 'unsafe-inline'",
"img-src 'self' data: blob:",
"font-src 'self' data: https:",
].join('; ')
const res = NextResponse.next({ request: { headers: new Headers({ ...req.headers, 'x-nonce': nonce }) } })
res.headers.set('content-security-policy', csp)
return res
}
// app/layout.tsx
import { headers } from 'next/headers'
import Script from 'next/script'

export default function RootLayout ({ children }: { children: React.ReactNode }) {
const nonce = headers().get('x-nonce') ?? ''
return (
<html>
<head>
<Script id="unovis-nonce" nonce={nonce} strategy="beforeInteractive">
{`window.UNOVIS_NONCE = ${JSON.stringify(nonce)}`}
</Script>
</head>
<body>{children}</body>
</html>
)
}

Known limitations

  • Inline style="…" attributes — CSP3 nonces do not apply to element style attributes. Because d3-selection sets them on every update (selection.style('fill', '#fff')), a strict style-src-attr breaks every chart. Use style-src-attr 'unsafe-inline' (or a per-page hash allowlist if you must — impractical for data-driven charts).
  • External stylesheets<link rel="stylesheet"> from third-party origins (e.g. Google Fonts, Bootstrap Icons CDN) cannot carry your server's nonce. Either self-host, add the origin to style-src-elem, or allow https:.
  • Web Workers from Blob URLs — the ELK-layered graph uses one. Add worker-src 'self' blob: if you render that component.
  • Runtime style injection by other libraries — the nonce is only applied to Unovis's own Emotion output. If you use Angular, MUI, Vuetify, Chakra, etc. in the same app, follow each library's own nonce-integration guide.

Verifying locally

The multi-framework gallery playground doubles as a CSP verification harness. From the repo root:

UNOVIS_CSP_NONCE=devnonce123 pnpm dev:gallery:csp

Then open http://localhost:9600 and inspect the Network tab — the response Content-Security-Policy header will show the strict policy, and every example (React, Vue, Solid, Svelte, TypeScript, Angular) should render with an empty DevTools console.