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:
- Loose CSP with
unsafe-inline - Nonce-based CSP
- Hash-based CSP
- Strict CSP with
strict-dynamic - 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-inlineinscript-srcdefeats 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', andframe-ancestors 'none' - Keep
connect-srctight - 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.
SaaS product with analytics and consent tooling
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.