Skip to content

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.

Ask your coding agent
Sample Consent banner with c15t’s English strings

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/integrations

The 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.app

Add 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.

src/consent-scripts.ts

import { gtag } from '@c15t/integrations/google-tag';

export const scripts = [gtag({ id: 'G-XXXXXXXXXX', category: 'measurement' })];

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.

  1. The first HTML contains the banner.
  2. Under an opt in policy, no PostHog request leaves the page before a choice.
  3. After Reject All and a reload, PostHog stays off.
  4. When you turn on measurement in Privacy settings, PostHog loads.
  5. 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.

See how the Inth consent banner works.

Questions about SvelteKit consent