Securing Svelte 5: Defeating 'unsafe-inline' Styles
A practical guide for removing 'unsafe-inline' styling from SvelteKit projects. Learn how to clear internal framework style violations, apply a locked-down 'style-src-attr' hash for the Svelte announcer, and handle dynamic runtime styles safely in Svelte 5.
Svelte’s Core Inline Style Collisions
Enforcing a strict Content Security Policy (CSP) inside modern websites and web-apps can be complex. Most teams default back to enabling style-src 'unsafe-inline' out of frustration because of how finnicky it can be to safely implement a strict CSP, especially across a large codebase with various third-party sources. When the browser hits a style attribute that lacks an authorised origin, the browser blocks the style from being applied to mitigate Cross-Site Scripting (XSS) risks. Below we’ll show how you can remove “unsafe-inline” from your style-src in a blank SvelteKit project.
Default Inline Styles
If you create a brand new SvelteKit project today (22 May 2026 - though this will likely be patched in the near future, see: github.com/sveltejs/kit/pull/14566), Svelte ships with 2 inline-styles.
One in the root app.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<!-- Inline Style #1 -->
<div style="display: contents">%sveltekit.body%</div>
</body>
</html> …and one injected as part of Svelte’s Announcer, used to aid screenreaders:
<!-- Inline Style #2, injected by SvelteKit -->
<div
id="svelte-announcer"
aria-live="assertive"
aria-atomic="true"
style="position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px"
>
<!---->
</div> Inline-Style #1: TailwindCSS Solution
If you are using TailwindCSS, you can easily remove inline style #1 by adding ‘app.html’ as a source in your ‘layout.css’…
@import 'tailwindcss';
@source '../app.html'; …then simply swap style="display: contents" for class="contents" in your app.html:
<body data-sveltekit-preload-data="hover">
<div class="contents">%sveltekit.body%</div>
</body> Inline-Style #1: Default Solution
If you’re not using Tailwind, you do not need to invent arbitrary global classes just to clear this rule. Because ‘app.html’ acts as the uncompiled HTML entry point for the framework, you can target this mounting structure natively using CSS child combinators.
Open your global stylesheet and explicitly match the direct structural container sibling within your body layout:
body > div {
display: contents;
} Then remove the styling from the div:
<body data-sveltekit-preload-data="hover">
<div>%sveltekit.body%</div>
</body> Inline-Style #2
Currently the simplest way to handle this and remove “unsafe-inline” from your CSP is to allow the specific hash corresponding to the styles that the announcer uses. As these styles are static, the following hash should work for your project: sha256-S8qMpvofolR8Mpjy4kQvEm7m1q8clzU4dfDH0AmvZjo=.
WARNING: If even a single character changes though (say in a future version of SvelteKit) this hash will be incorrect. If this has changed, you can set your style-src to ‘self’, and then check your browser’s console. It will say which element is violating the CSP and give you the exact hash needed.
NOTE: To allow hashes in your CSP, you’ll need to add the origin ‘unsafe-hashes’ to your style-src directive as well as the hash.
style-src-attr Directive
You can increase the security posture of this marginally by using modern CSPs ‘style-src-attr’ directive instead of ‘style-src’, limiting the scope of ‘unsafe-hashes’ specifically to the attribute directive:
csp: {
directives: {
'style-src': [
"'self'",
],
'style-src-attr': [
"'unsafe-hashes'",
// Hardcoded hash for SvelteKit's internal announcer
"'sha256-S8qMpvofolR8Mpjy4kQvEm7m1q8clzU4dfDH0AmvZjo='",
],
},
}, Removing the Rest of Your inline-styles
The easiest way to check if you yourself are applying any inline-styles is to utilise ESLint. (If you didn’t install this when scaffolding your project, install it using npx sv add eslint/pnpm dlx sv add eslint). In your eslint.config.js you can set the rule 'svelte/no-inline-styles': 'error', and that’ll flag any when you run pnpm run check.
export default defineConfig({
rules: {
'svelte/no-inline-styles': 'error',
},
}); To bypass inline style mutations, you need to decouple structural styles from runtime calculation loops. If your layout uses dynamic values (like interactive cursor tracking, GSAP timelines etc), do not use Svelte’s reactive template styles like style:transform="{x}px".
Instead, to maintain a strict CSP you’ll need to leverage direct mutations inside Svelte 5 $effect blocks via element bindings, or offload layout math entirely to external global utility sheets compiled at build time.
The Full svelte.config.js
Here is the complete default configuration file (minus runes configuration) for svelte.config.js, with the addition of a CSP that blocks inline styles in SvelteKit’s minimal template:
import adapter from '@sveltejs/adapter-auto';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter(),
csp: {
mode: 'auto',
directives: {
'default-src': ["'self'"],
'style-src': ["'self'"],
'style-src-attr': [
"'unsafe-hashes'",
// Hardcoded hash for SvelteKit's internal announcer
"'sha256-S8qMpvofolR8Mpjy4kQvEm7m1q8clzU4dfDH0AmvZjo='",
],
},
},
},
};
export default config;