Skip to main content
Back to Guides
Compliance5 min read

SvelteKit Cookie Consent: The 2026 Setup Guide

SvelteKit renders on the server and hydrates on the client, which means a tracker in app.html or an onMount call can fire before consent. Here's where the consent loader belongs, how to block scripts until opt-in, and how to check consent in hooks.server.ts.

SvelteKit renders a page on the server, sends HTML, then hydrates it in the browser. That two-step timing is where cookie consent tends to break. Drop a Google Analytics snippet into app.html and it runs on every page as the document loads. Put a Meta Pixel init in a component's onMount and it fires the instant that component mounts. Neither waits for a visitor to agree.

Regulators treat that as tracking without consent. France's CNIL fined SHEIN 150 million euros in September 2025 partly because trackers fired before any choice and continued after "Reject all" (CNIL). If your SvelteKit app serves the EU, the UK, or a US opt-out state, you need a banner that actually holds tags back, plus a way to check consent on the server before you send data yourself.

app.html is your page shell

Every SvelteKit page is rendered into src/app.html. It holds placeholders that SvelteKit fills at render time: %sveltekit.head% injects the <link> and <script> elements the app needs plus anything from <svelte:head>, and %sveltekit.body% holds the rendered markup (SvelteKit project structure). There's also %sveltekit.nonce% for a CSP nonce if you run a content security policy.

A script you write directly into app.html loads on every page and isn't bundled by Vite the way component scripts are. That makes app.html the right home for the consent loader.

Where the consent loader goes

Place the loader in the <head> of app.html, before %sveltekit.head% and before any analytics or marketing tag. It has to initialize first, or it has nothing to gate.

<!-- src/app.html -->
<head>
  <meta charset="utf-8" />
  <!-- FIRST, before %sveltekit.head% and any tracker -->
  <script async src="https://cdn.cookiebeam.com/banner/YOUR_BANNER_ID/default/loader.js"></script>
  %sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
  <div>%sveltekit.body%</div>
</body>

Swap YOUR_BANNER_ID for your banner's public ID from the dashboard.

Block trackers until consent

Load trackers in a disabled state and switch them on after opt-in. CookieBeam's loader activates any script tagged type="text/plain" with a data-category once the visitor accepts that category. Add these in app.html so they're present on every page:

<!-- src/app.html, in <head> after the loader -->
<script
  type="text/plain"
  data-category="analytics"
  data-cookiebeam-managed="true"
  src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX">
</script>

The browser ignores a text/plain script until the consent runtime rewrites it, so nothing loads on a rejected visit. See how to block scripts before consent for the mechanics.

Reading consent inside components

When a component needs to react to consent (mount a chat widget, load a map, start a heatmap), subscribe to the cookiebeam:consent event and mirror it into a Svelte store so the rest of your UI stays in sync:

// src/lib/consent.js
import { writable } from 'svelte/store';

export const consent = writable({ analytics: false, marketing: false, preferences: false });

if (typeof window !== 'undefined') {
  window.addEventListener('cookiebeam:consent', (e) => {
    consent.set(e.detail);
  });
}
<!-- A component that only loads a map with consent -->
<script>
  import { consent } from '$lib/consent.js';
</script>

{#if $consent.preferences}
  <iframe title="map" src="https://maps.example.com/embed"></iframe>
{/if}

Guard the window reference with a typeof window check so the store doesn't throw during server rendering.

Server-side checks in hooks.server.ts

Client-side blocking stops scripts in the browser. It does nothing about tracking you trigger from your own server, a conversion event, a server-side pixel, a log to an analytics warehouse. SvelteKit's hooks.server.ts runs on every request and can read the consent cookie:

// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';

export const handle: Handle = async ({ event, resolve }) => {
  const raw = event.cookies.get('cookiebeam_consent');
  let analytics = false;
  if (raw) {
    try { analytics = JSON.parse(raw).analytics === true; } catch { /* no consent */ }
  }
  event.locals.analyticsConsent = analytics;
  return resolve(event);
};

Now a load function or form action can check event.locals.analyticsConsent before sending anything server-side. One caveat: the cookie reflects consent at the time of the request, so if a visitor changes their choice without loading a new page, the server sees the old value until the next request. Server checks complement client-side blocking, they don't replace it. For the deeper pattern, see server-side consent enforcement.

Consent Mode v2 with GTM

For Google Ads or GA4 on EEA or UK traffic, Consent Mode v2 has been required since March 2024. Set defaults to denied in app.html before GTM loads, then let the banner send the update on acceptance:

<!-- src/app.html, after the consent loader -->
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('consent', 'default', {
    ad_storage: 'denied',
    analytics_storage: 'denied',
    ad_user_data: 'denied',
    ad_personalization: 'denied',
    wait_for_update: 500
  });
</script>
<!-- GTM container snippet goes here -->

CookieBeam applies the defaults and fires gtag('consent', 'update', ...) when the visitor chooses. The GTM consent setup guide walks through the container side.

Policy and testing

Publish a /cookie-policy route and link it from the banner and footer. Keep it accurate as you add embeds and widgets, or let a scanner maintain the cookie table for you (scanning versus manual audits). Then test the production build, not the dev server:

  • Load a fresh private window from an EU IP and confirm only necessary cookies exist before you click.
  • Reject, and verify no _ga or _fbp appears and no beacons fire.
  • Accept, and confirm the tags load and Consent Mode reports granted.
  • Move between routes (SvelteKit does client-side routing) and re-check that state persists correctly.

CookieBeam blocks unknown scripts by default and stores a timestamped consent record, so a SvelteKit app clears the same audit a purpose-built platform would. Run a full consent audit after any new integration, and if you also render analytics islands in other frameworks, the SPA consent guide covers route-change timing.

SvelteKit Cookie Consent: 2026 Setup Guide | CookieBeam | CookieBeam