Add a cookie consent banner to SvelteKit
c15t puts the banner in your SvelteKit app as code. It resolves consent on the server, holds tags until the visitor chooses and keeps Google Consent Mode v2 in sync.
Install c15t in a SvelteKit app
c15t is the open source consent library behind Inth Consent. It renders the banner on the server and records each choice in your Inth project.
npm install c15t @c15t/integrationsThe components come from c15t/svelte. The server helpers come from c15t/svelte/kit and the Vite plugin from c15t/svelte/vite.
Add your Inth project URL to .env. Copy it from your Inth dashboard.
.env
PUBLIC_C15T_BACKEND_URL=https://your-project.inth.appAdd consentManifest to the plugins in vite.config.ts. It downloads your policy during vite build and keeps it on the server. Rebuild after you change a policy.
vite.config.ts
import adapter from '@sveltejs/adapter-auto';
import { sveltekit } from '@sveltejs/kit/vite';
import { consentManifest } from 'c15t/svelte/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [consentManifest(), sveltekit({ adapter: adapter() })],
});On SvelteKit 2, keep the adapter in svelte.config.js and do not give it to sveltekit().
Code on this page comes from the c15t SvelteKit quickstart, checked 28 October 2026.
Add the server hook
c15tHandle reads the consent cookie, the location headers and Global Privacy Control one time for each request. It also writes the banner’s styles into the HTML <head>.
src/hooks.server.ts
import { c15tHandle } from 'c15t/svelte/kit';
export const handle = c15tHandle();Type event.locals.c15t with one line in src/app.d.ts.
src/app.d.ts
/// <reference types="c15t/svelte/kit/locals" />Resolve consent in the root layout
Export loadConsent as the server load of the root layout. It resolves the policy for the visitor from the bundled snapshot, so a page render sends no policy request to the backend.
src/routes/+layout.server.ts
export { loadConsent as load } from 'c15t/svelte/kit';Then render ConsentRoot in src/routes/+layout.svelte and give it data.consent unchanged.
src/routes/+layout.svelte
<script lang="ts">
import { posthog } from '@c15t/integrations/posthog';
import {
ConsentBanner,
ConsentDialog,
ConsentDialogLink,
ConsentRoot,
} from 'c15t/svelte';
let { children, data } = $props();
const scripts = [
posthog({
id: 'phc_your_project_key',
initOptions: { cookieless_mode: 'never' },
loadMode: 'after-consent',
}),
];
</script>
<ConsentRoot state={data.consent} {scripts}>
{@render children()}
<footer>
<ConsentDialogLink>Privacy settings</ConsentDialogLink>
</footer>
<ConsentBanner />
<ConsentDialog />
</ConsentRoot>Create scripts in the layout component, not in +layout.server.ts. A server load cannot send functions to the browser. Keep ConsentRoot in the root layout, so it stays mounted across navigation.
Hold scripts until consent
With loadMode: 'after-consent', PostHog waits for measurement permission. Remove any PostHog snippet from src/app.html, so c15t is the only thing that loads it.
SvelteKit scripts and embeds covers more vendors, iframes and the network blocker. The c15t integrations list the helper for each vendor.
Prerendered pages and static sites
A prerendered page goes to every visitor, so it cannot contain the consent of one visitor.
On those pages, the browser resolves the policy after hydration. Optional categories stay denied until then. Server rendered pages in the same app keep the banner in their HTML.
src/routes/about/+page.ts
// Build this page once as static HTML. While SvelteKit prerenders,
// `loadConsent` returns no visitor's consent, so the browser resolves it
// here.
export const prerender = true;With adapter-static, SvelteKit prerenders every page. Set hosted() as the mode in the hook, so the browser asks the backend directly.
src/hooks.server.ts
import { c15tHandle, hosted } from 'c15t/svelte/kit';
export const handle = c15tHandle({ mode: hosted() });SvelteKit rendering also covers SPA mode and edge adapters.
Google Consent Mode v2 in SvelteKit
The Google tag helper sends the Consent Mode v2 default before your config and an update after each choice. It ships in @c15t/integrations.
src/consent-scripts.ts
import { gtag } from '@c15t/integrations/google-tag';
export const scripts = [gtag({ id: 'G-XXXXXXXXXX', category: 'measurement' })];Add the helper to the scripts array in src/routes/+layout.svelte, next to PostHog.
Measurement maps to analytics_storage. Marketing maps to ad_storage, ad_user_data and ad_personalization.
By default the helper loads gtag.js before a choice. Google then gets a default that matches the current permissions. Set loadMode: 'after-consent' to send nothing to Google until the visitor accepts.
Show the right banner in each region
Your Inth project sets a policy for each region, such as opt in for the EU and UK or opt out for California. On the server, c15tHandle reads the location headers of each request, and loadConsent applies the policy for that location.
Test each region before launch. See how regional policies work.
Keep the banner in the first HTML
The root layout resolves consent on the server before the page renders, so the banner is part of the first HTML. loadConsent never stops the render. If it takes longer than 500 ms, it returns the stored choice without a policy, and the browser resolves the policy after hydration.
Measure your own pages before and after you add the banner.
Check that consent gates work
Build with vite build, serve the build with vite preview and open a private window with the Network tab open. Test under an opt in policy, such as the EU policy.
- The first HTML contains the banner.
- Under an opt in policy, no PostHog request leaves the page before a choice.
- After Reject All and a reload, PostHog stays off.
- When you turn on measurement in Privacy settings, PostHog loads.
- When you turn measurement off, the page reloads without PostHog.
Choices post to your Inth project. Check that Inth has a record for each one. The c15t verify consent guide has the full release checklist.
Use Inth in production
c15t is open source. Point it at your Inth project, which hosts the backend and keeps the consent records. See Inth open source.
c15t also has a local mode with no backend. It keeps no consent records, so the docs advise against it in production.
Want to see what else your repo sends before consent? Run a free codebase scan.
Cookie consent banners for other frameworks
The same banner and the same Inth project work in other stacks. See the setup for Next.js, React, TanStack Start, Nuxt, Vue, Astro, Svelte, HTML and JavaScript.
Questions about SvelteKit consent
Yes. On a prerendered page, the browser resolves consent after hydration. Optional categories stay denied until then.
Yes. Set hosted() as the mode in c15tHandle. The browser then asks the backend for the policy.
SvelteKit 2.63 and later, including SvelteKit 3, with Svelte 5.20 and later.
A server load cannot send functions to the browser. Vendor helpers contain functions, so create them in +layout.svelte.
Use the c15t Google tag helper. It sends the default before your config and an update after each choice.
Yes. Set a policy for each region in your Inth project. c15t applies the policy that matches the visitor’s location. See regional policies.
Yes. See the Svelte cookie consent banner.
c15t has a local mode with no backend, but it keeps no consent records. Use your Inth project in production.