shadcn/ui gives you a weird CSP problem compared to most component libraries: it is not really a library in the classic sense. You copy components into your app, own the code, and then your CSP story becomes your problem.

That is good for flexibility, but it also means there is no single “shadcn/ui CSP policy.” The right policy depends on how you render styles, whether you use theme scripts, whether you pull in analytics, and whether your app is static, SSR, or edge-rendered.

I’ve seen teams treat CSP as an afterthought until they enable it and half the UI breaks. With shadcn/ui, the biggest friction usually comes from:

  • inline theme scripts
  • inline styles or style attributes
  • third-party analytics
  • Next.js hydration and nonce handling
  • copied component code that later grows custom script behavior

Here’s the practical comparison guide I wish more teams started with.

The core CSP options for shadcn/ui

For most shadcn/ui apps, you’ll end up choosing one of these approaches:

  1. Loose CSP with unsafe-inline
  2. Nonce-based CSP
  3. Hash-based CSP
  4. Strict CSP with strict-dynamic
  5. Hybrid CSP for real production apps

I’ll compare them based on security, developer pain, and how well they fit the typical shadcn/ui + Next.js stack.


Option 1: Loose CSP with unsafe-inline

This is the “make it work fast” setup.

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

Pros

  • Very easy to roll out
  • Usually works with theming code and style-heavy UI right away
  • Minimal framework plumbing

Cons

  • Weak protection against XSS
  • unsafe-inline in script-src defeats a big part of why you enabled CSP
  • Easy for the policy to slowly become meaningless as more third parties get added

Good fit

  • Internal tools
  • Temporary migration step
  • Teams that need CSP reporting first and enforcement later

Bad fit

  • Public-facing apps handling user content
  • Security-sensitive SaaS
  • Anything where you actually care about XSS mitigation

For shadcn/ui specifically, style-src 'unsafe-inline' is pretty common because many apps rely on inline style behavior or inject style attributes somewhere in the tree. script-src 'unsafe-inline' is the one I try hard to avoid.


Option 2: Nonce-based CSP

This is usually the best balance for shadcn/ui apps using Next.js.

A nonce lets you allow specific inline scripts or styles generated during the request.

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

Then attach the nonce to inline script tags:

<script nonce={nonce} dangerouslySetInnerHTML={{
  __html: `
    try {
      const theme = localStorage.getItem('theme');
      if (theme === 'dark') document.documentElement.classList.add('dark');
    } catch (e) {}
  `
}} />

Pros

  • Stronger than unsafe-inline
  • Works well for request-based rendering
  • Great for inline theme bootstrapping scripts
  • Scales better than hashes when script content changes

Cons

  • More setup in Next.js middleware, headers, and rendering
  • Harder with fully static output
  • You must reliably pass the nonce everywhere it is needed

Good fit

  • Next.js SSR apps using shadcn/ui
  • Apps with a dark mode/theme boot script
  • Teams that want strong CSP without insane maintenance

Bad fit

  • Pure static sites with no server-generated nonce
  • Teams that cannot control response headers dynamically

If you use a theme script to prevent dark-mode flash, nonce-based CSP is usually the sane choice.

Here’s a rough Next.js middleware example:

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

function generateNonce() {
  return Buffer.from(crypto.randomUUID()).toString("base64");
}

export function middleware(req: NextRequest) {
  const nonce = generateNonce();

  const csp = [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}'`,
    `style-src 'self' 'nonce-${nonce}'`,
    "img-src 'self' data: https:",
    "font-src 'self'",
    "connect-src 'self'",
    "object-src 'none'",
    "base-uri 'self'",
    "frame-ancestors 'none'",
  ].join("; ");

  const requestHeaders = new Headers(req.headers);
  requestHeaders.set("x-nonce", nonce);

  const response = NextResponse.next({
    request: { headers: requestHeaders },
  });

  response.headers.set("Content-Security-Policy", csp);
  return response;
}

Then read the nonce in your layout or document layer and apply it to scripts.


Option 3: Hash-based CSP

Hashes allow exact inline script or style blocks by content.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'sha256-AbCdEf123...';
  style-src 'self' 'sha256-ZyXwVu456...';

Pros

  • Very strong for fixed inline snippets
  • Great for static deployments
  • No per-request nonce generation required

Cons

  • Fragile when inline content changes
  • Painful with generated or frequently edited scripts
  • Easy to break during refactors or formatting changes

Good fit

  • Static marketing sites using a tiny fixed theme script
  • Teams with a very stable inline bootstrap snippet
  • Build pipelines that can automatically compute hashes

Bad fit

  • Fast-moving product apps
  • Apps with multiple inline snippets
  • Anything where inline content is generated dynamically

For shadcn/ui, hash-based CSP works if your only inline script is a stable theme initializer. If that script changes every other sprint, hashes become annoying fast.


Option 4: Strict CSP with strict-dynamic

This is the more advanced version of nonce-based CSP and a solid choice if you load trusted bootstrap scripts that then load others.

A real production header from Headertest uses this pattern:

content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-MTg4NDhmM2YtNGJmMy00ZmZjLWI2ZWItNzYzMWViODQ3OGRk' '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'

If you want to inspect your own headers and see how they hold up, Headertest is handy for quick validation.

Pros

  • Strong modern CSP model
  • Better for apps that need third-party script loaders
  • Lets trusted nonce-bearing scripts load dependencies safely

Cons

  • Harder to understand
  • Browser support history is more nuanced than basic nonce/hash setups
  • Can confuse teams who copy directives without understanding them

Good fit

  • Mature apps with analytics, consent tools, and script loaders
  • Teams comfortable debugging CSP behavior
  • Security-conscious production systems

Bad fit

  • Small apps with simple requirements
  • Teams still learning CSP basics

If strict-dynamic is new to you, csp-guide.com is a good place to brush up on directive behavior without reading the spec directly.


Option 5: Hybrid CSP for real production shadcn/ui apps

This is the one I recommend most often.

Typical shape:

  • nonce-based script-src
  • maybe strict-dynamic
  • style-src 'self' 'unsafe-inline' for pragmatic compatibility
  • tight connect-src
  • object-src 'none'
  • base-uri 'self'
  • frame-ancestors 'none'
  • form-action 'self'

Example:

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

Pros

  • Strong script protections where they matter most
  • Less friction from CSS and UI styling edge cases
  • Realistic for shadcn/ui apps shipping today

Cons

  • Not the cleanest “perfect CSP”
  • style-src 'unsafe-inline' is a compromise
  • You still need discipline around third-party sources

Good fit

  • Most SaaS apps using shadcn/ui
  • Teams that want security without breaking velocity
  • Apps with analytics, theming, and modern frontend tooling

This is the policy shape I reach for first unless there is a reason to go stricter.


My opinionated recommendation

For most shadcn/ui projects:

  • Avoid script-src 'unsafe-inline'
  • Use nonces for scripts
  • Accept style-src 'unsafe-inline' if needed, at least initially
  • Add object-src 'none', base-uri 'self', and frame-ancestors 'none'
  • Keep connect-src tight
  • Review every third-party domain instead of dumping them into default-src

That gives you meaningful XSS protection without turning your UI stack into a CSP science project.

Common shadcn/ui CSP breakpoints

1. Theme initialization script

A lot of apps add a tiny inline script to set dark mode before hydration. That script needs a nonce or hash.

2. Third-party analytics

Google Tag Manager, analytics SDKs, consent platforms, and chat widgets are where policies get messy fast. Put them in the right directives, not just default-src.

3. Inline styles

Some UI patterns and ecosystem packages still rely on inline styles. If you can avoid them, great. If not, be honest and allow them only in style-src.

4. WebSockets and API calls

If your shadcn/ui dashboard talks to real-time backends, don’t forget wss: or explicit WebSocket origins in connect-src.


Best choice by scenario

Static shadcn/ui site

Use hash-based CSP if inline scripts are tiny and stable.

Next.js SSR app

Use nonce-based CSP.

Use a hybrid CSP with nonce + maybe strict-dynamic.

Security-sensitive app

Start with nonce-based or strict CSP, and fight hard to remove unnecessary inline behavior.

The biggest mistake is trying to copy a “perfect CSP” from another app. shadcn/ui is too app-specific for that. Build the policy around how your components, scripts, and third parties actually work. That’s less glamorous than pasting a template, but it’s the difference between a CSP that protects you and one that just looks nice in a header dump.