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

Source: https://cookiequiet.com/platforms/sveltekit
Published: 2026-10-21
Publisher: Cookiequiet

## Steps

1. **Copy the snippet** In the Cookiequiet dashboard, add the website and copy the snippet with your site key, data-consent-mode="1" and data-auto-block="1".
2. **Paste it in src/app.html** Open 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%.
3. **Mark your tags** Paste analytics and advertising tags below the snippet in app.html with type="text/plain" and data-cq-category set to analytics or marketing.
4. **Gate components** Create 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.
5. **Gate page views** If your analytics needs a page view per route, send it from afterNavigate in the root +layout.svelte only when analytics is granted.
6. **Add the Cookie preferences link** In the footer, add a plain anchor with href="#cq-preferences" and the data-cq-show-preferences attribute.
7. **Test with Reject all** Run 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.

## How do I block my tags until consent?

**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](/platforms/meta-pixel) explains why. To keep the Google tag under Consent Mode instead, see the [Google Analytics 4 guide](/platforms/google-analytics-4).

**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`.

## How do I add the Cookie preferences link?

```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

### Why not add the snippet in +layout.svelte with svelte:head?

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.

### Does it work with adapter-static and prerendering?

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

### How do I read the visitor's choice in Svelte?

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.

### Does the banner come back on each client-side navigation?

No. The script in app.html runs once per full page load and keeps the choice applied while SvelteKit swaps routes.

## Sources

- [SvelteKit docs: Project structure (src/app.html placeholders)](https://svelte.dev/docs/kit/project-structure)
- [SvelteKit docs: $app/navigation (afterNavigate)](https://svelte.dev/docs/kit/$app-navigation)
- [SvelteKit docs: $app/environment](https://svelte.dev/docs/kit/$app-environment)
