Ariakit is a headless UI toolkit, which is good news for CSP. It doesn’t force a CSS-in-JS runtime, it doesn’t need eval, and it generally stays out of your way. That means you can run a pretty strict Content Security Policy without fighting your component library.

The tricky part usually isn’t Ariakit itself. It’s your app shell, analytics, consent tools, inline bootstrapping scripts, and whatever your framework injects during hydration.

I’ve seen teams blame the UI library when the real problem was a tag manager or a framework-generated inline script. Ariakit is usually the easy part.

What CSP needs to cover in an Ariakit app

If you’re building with Ariakit, your CSP usually needs to account for:

  • Your own scripts and styles
  • Any inline scripts your framework emits
  • Portals, dialogs, menus, and popovers rendered by React
  • Analytics or consent tools
  • API calls
  • Fonts and images
  • Framing restrictions

Ariakit components like Dialog, Menu, Popover, and Combobox don’t need special CSP directives by themselves. They render DOM, manage focus, and use standard React patterns. CSP only starts getting interesting when your app adds:

  • inline <script>
  • inline <style>
  • third-party scripts
  • style injection from CSS-in-JS libraries
  • dynamic code loading patterns

A real CSP header to learn from

Here’s a real-world 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-MWFiY2IwMTktODhiNC00ZmUwLThkNTUtMGJkNjQ4MTg0YjY4' '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'

This is a decent production-style policy because it does a few things right:

  • uses a nonce in script-src
  • uses strict-dynamic
  • blocks plugins with object-src 'none'
  • locks down embedding with frame-ancestors 'none'
  • constrains forms and base URLs

It also has one thing I’d try to remove if possible:

style-src 'self' 'unsafe-inline'

That’s common, but I don’t love it. If your Ariakit app can avoid inline styles, you can tighten this.

For directive details, the official CSP docs are still the baseline, and https://csp-guide.com is useful for quick directive explanations.

A minimal CSP for an Ariakit app

If your app uses Ariakit, static CSS files, and no third-party scripts, start here:

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

That’s enough for a surprising number of apps.

Ariakit itself won’t require unsafe-inline or unsafe-eval.

React + Ariakit example

Here’s a simple Ariakit dialog:

import * as Ariakit from "@ariakit/react";
import { useState } from "react";

export default function DeleteDialog() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button onClick={() => setOpen(true)}>Delete account</button>

      <Ariakit.Dialog open={open} onClose={() => setOpen(false)}>
        <Ariakit.DialogHeading>Delete account</Ariakit.DialogHeading>
        <p>This action cannot be undone.</p>
        <button onClick={() => setOpen(false)}>Cancel</button>
        <button>Confirm</button>
      </Ariakit.Dialog>
    </>
  );
}

Nothing about this component needs special CSP handling. If this breaks under CSP, the issue is almost certainly elsewhere.

Where CSP usually breaks with Ariakit apps

1. Inline bootstrapping scripts

A lot of React frameworks emit inline scripts for hydration, routing state, or serialized data.

This will fail under a strict CSP unless you allow it with a nonce or hash:

<script>
  window.__BOOTSTRAP__ = { userId: 123 };
</script>

The right fix is usually a nonce.

Using nonces with server-rendered React

Generate a per-request nonce on the server:

import crypto from "node:crypto";

export function generateNonce() {
  return crypto.randomBytes(16).toString("base64");
}

Build the CSP header with that nonce:

export function buildCsp(nonce: string) {
  return [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
    `style-src 'self' 'nonce-${nonce}'`,
    "img-src 'self' data: https:",
    "font-src 'self'",
    "connect-src 'self'",
    "frame-ancestors 'none'",
    "base-uri 'self'",
    "form-action 'self'",
    "object-src 'none'",
  ].join("; ");
}

Then apply it in Express:

import express from "express";
import { generateNonce, buildCsp } from "./csp";

const app = express();

app.use((req, res, next) => {
  const nonce = generateNonce();
  res.locals.nonce = nonce;
  res.setHeader("Content-Security-Policy", buildCsp(nonce));
  next();
});

app.get("/", (req, res) => {
  const nonce = res.locals.nonce;

  res.send(`
    <!doctype html>
    <html>
      <head>
        <meta charset="utf-8" />
        <title>Ariakit App</title>
        <script nonce="${nonce}">
          window.__BOOTSTRAP__ = { featureFlags: ["dialog"] };
        </script>
        <link rel="stylesheet" href="/app.css" />
      </head>
      <body>
        <div id="root"></div>
        <script nonce="${nonce}" src="/app.js"></script>
      </body>
    </html>
  `);
});

If your framework supports nonce propagation into generated script tags, use that instead of hand-rolling HTML.

What about strict-dynamic?

I’m a fan of strict-dynamic when you’re already using nonces.

Example:

script-src 'self' 'nonce-abc123' 'strict-dynamic';

With strict-dynamic, trusted nonce-bearing scripts can load other scripts, and the browser extends trust accordingly. This is often cleaner than maintaining a giant allowlist.

That said, browser support and framework behavior matter. Test it in the browsers you care about. Don’t just copy a policy from another site and hope.

Styling Ariakit without weakening CSP

Ariakit doesn’t ship a styling system that forces inline styles. That gives you options:

  • plain CSS files
  • CSS Modules
  • Tailwind-generated static CSS
  • build-time extracted CSS

Those all work nicely with:

style-src 'self';

or, if you truly need nonce-based inline styles:

style-src 'self' 'nonce-abc123';

What I try to avoid is this:

style-src 'self' 'unsafe-inline';

Sometimes you inherit a framework or third-party widget that makes this hard to remove. Fine. Ship it if you must. But don’t pretend it’s ideal.

If you use a CSS-in-JS runtime that injects <style> tags at runtime, check whether it supports CSP nonces. If not, that library is now your CSP problem.

Third-party scripts in Ariakit apps

Most CSP pain comes from stuff like analytics, A/B testing, and consent banners.

Using the headertest.com policy as a reference, a production app might need something like:

Content-Security-Policy:
  default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  script-src 'self' 'nonce-REPLACE_ME' '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.example.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';

A few opinions here:

  • Keep third-party hosts out of default-src if you can. Be specific per directive.
  • Don’t add domains “just in case.”
  • connect-src tends to grow quietly over time. Audit it.
  • Consent tools often require frame-src, script-src, style-src, and connect-src. They’re rarely lightweight.

A tighter version would look more like this:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-REPLACE_ME' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  style-src 'self' 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;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none';

If that breaks because of inline styles, you now know exactly what to fix.

Debugging CSP violations

Use report-only mode before enforcing:

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' 'nonce-abc123' 'strict-dynamic';
  style-src 'self';
  img-src 'self' data:;
  connect-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

You can also add reporting directives if your stack supports them.

When something breaks, check:

  • Was it an inline script?
  • Was it an injected style tag?
  • Was it a fetch or websocket blocked by connect-src?
  • Was a third-party iframe blocked by frame-src?
  • Did your server generate the nonce but fail to attach it to all script tags?

That last one bites people constantly.

A practical CSP recipe for Ariakit

If I were shipping an Ariakit app today, I’d aim for this:

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

Then I’d relax it only where the app proves it’s necessary.

That’s the right mindset with Ariakit too: start strict, because the component library usually won’t be the thing forcing you to weaken policy. If your CSP ends up messy, look at your framework and third-party dependencies first. Ariakit is probably innocent.