Platforms

How to add a cookie consent banner to a SvelteKit app

In SvelteKit, paste the snippet into src/app.html, the page template every route uses, above the %sveltekit.head% placeholder so it comes before SvelteKit's own scripts. Mark tracking tags in the same file with type="text/plain" and data-cq-category, and read the cq:consent event in components that load embeds or send page views after client-side navigation.
Published 21 October 2026 · By the Cookiequiet team · Markdown

Steps

Copy the snippetIn the Cookiequiet dashboard, add the website and copy the snippet with your site key, data-consent-mode="1" and data-auto-block="1".
Paste it in src/app.htmlOpen src/app.html and paste the snippet inside the head, above %sveltekit.head%. Optionally read the key from a PUBLIC_ environment variable with %sveltekit.env.PUBLIC_COOKIEQUIET_SITE%.
Mark your tagsPaste analytics and advertising tags below the snippet in app.html with type="text/plain" and data-cq-category set to analytics or marketing.
Gate componentsCreate a small store that listens to the cq:consent event and render embeds such as YouTube or a chat widget only when their category is granted.
Gate page viewsIf your analytics needs a page view per route, send it from afterNavigate in the root +layout.svelte only when analytics is granted.
Add the Cookie preferences linkIn the footer, add a plain anchor with href="#cq-preferences" and the data-cq-show-preferences attribute.
Test with Reject allRun the app, open it in a private window, click Reject all and check that no _ga or _fbp cookie is set. Accept all, reload, navigate, and check the consent log.

Where does the snippet go in SvelteKit?

SvelteKit's page template is src/app.html. The %sveltekit.head% placeholder is replaced with the <link> and <script> elements the app needs, plus any <svelte:head> content. Put the snippet above it, so it is in the HTML of every route before SvelteKit's scripts:

html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://cookiequiet.com/cq.js" data-site="YOUR_SITE_KEY" data-consent-mode="1" data-auto-block="1" defer></script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

To keep the key out of the file, write data-site="%sveltekit.env.PUBLIC_COOKIEQUIET_SITE%". SvelteKit replaces the placeholder at render time with a public environment variable, and with an empty string if it is missing. With a nonce-based Content Security Policy, add nonce="%sveltekit.nonce%" to the tag, and allow https://cookiequiet.com in script-src and connect-src.

Do not load the snippet with <svelte:head> in a layout. That content is placed by the app among its own head elements, while app.html is the one template that puts the script before everything else on every route.

Marked tags in app.html. Paste them under the snippet and change their opening tag. Cookiequiet runs them when the category is granted, and at once on later visits where the stored choice includes it:

html
<script type="text/plain" data-cq-category="analytics" src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"></script>
<script type="text/plain" data-cq-category="analytics">
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXX');
</script>

The Meta Pixel base code takes data-cq-category="marketing", without its noscript image; the Meta Pixel guide explains why. To keep the Google tag under Consent Mode instead, see the Google Analytics 4 guide.

Scripts created by JavaScript. Tag managers and widgets that inject trackers at runtime are held by the auto-blocker (switched on by data-auto-block="1") when the tracker is one of the about 80 it knows, until the category is granted.

How do components know the visitor's choice?

Cookiequiet dispatches a cq:consent event on window each time it applies a choice: a stored one when the page loads, a new one after a click, and every category at once in regions where geo-targeting does not show the banner. event.detail.categories lists the granted categories. A store wraps it:

js
// src/lib/consent.js
import { readable } from "svelte/store";
import { browser } from "$app/environment";
​
export const consent = readable([], (set) => {
if (!browser) return;
const stored = window.Cookiequiet && window.Cookiequiet.get();
if (stored) set(stored.c);
const onConsent = (e) => set(e.detail.categories);
window.addEventListener("cq:consent", onConsent);
return () => window.removeEventListener("cq:consent", onConsent);
});

In a component, render the embed only when the category is in the list:

svelte
<script>
import { consent } from "$lib/consent";
</script>
​
{#if $consent.includes("marketing")}
<iframe src="https://www.youtube-nocookie.com/embed/VIDEO_ID" title="Video"></iframe>
{:else}
<button onclick={() => window.Cookiequiet.showPreferences()}>Allow videos</button>
{/if}

What about client-side navigation?

SvelteKit navigates without full reloads after the first page. The snippet runs once per full load, the choice stays applied and the banner does not reappear. Marked scripts are activated on load and after each choice, so keep them in app.html, not in route components. If your analytics needs a page view per route, afterNavigate from $app/navigation, called in the root +layout.svelte, runs on mount and after every navigation: read get(consent) there (get comes from svelte/store) and send the page view only when it includes analytics.

html
<a href="#cq-preferences" data-cq-show-preferences>Cookie preferences</a>

Cookiequiet listens for clicks on this link anywhere on the page and opens the preferences centre.

How do I test it?

  1. Run vite dev or build and preview, and open the app in a private window.
  2. View the page source: the Cookiequiet script comes before SvelteKit's scripts.
  3. Click Reject all. The cookies are cq_consent and your session cookies, nothing from Google or Meta.
  4. Clear the cookies, click Accept all, reload, navigate between routes: the tags run and the choice holds.
  5. Check the consent log in the dashboard.

Prerendered pages and adapters (adapter-static, adapter-node, adapter-vercel) all use the same template, so nothing changes per adapter. Cookiequiet is free for one website with geo-targeting included; Starter costs €3 per website per month, with unlimited pageviews on every plan.

Questions

Content in svelte:head is managed by the app and placed with its own head elements. The consent script must sit before the tags it controls on every page, which app.html guarantees because it is the template of every route.
Yes. The banner is a client script and app.html is the template of prerendered pages too. The site key can come from a PUBLIC_ environment variable through %sveltekit.env.PUBLIC_COOKIEQUIET_SITE%.
Listen for the cq:consent event on window, whose detail.categories lists the granted categories, and start from window.Cookiequiet.get() for a stored choice. A readable store that wraps both is shown above.
No. The script in app.html runs once per full page load and keeps the choice applied while SvelteKit swaps routes.

Put a quiet banner on your website today

Free for one website with every core feature, no card and no trial clock. Upgrade a website to Starter for $3 a month to match your brand with AI and drop the branding, or to Pro for $9 to design it with the AI agent.