Svelte & SvelteKit
This guide sets up ConsentLayer in a SvelteKit application: a module-level analytics instance, a root layout that holds consent state for your consent banner, page views on every navigation, event tracking, and error reporting. ConsentLayer supports Svelte 5.14.3 and later, with runes, with two entry points:
consentlayer/sveltekitfollows SvelteKit’s navigation itself and addshandleErrorWith()for the clienthandleErrorhook.consentlayer/svelteexports the same functions and components, for a Svelte application without SvelteKit. Its differences are in Without SvelteKit.
It assumes you have installed consentlayer and the
SDKs of the providers you use.
1. Create the instance
Section titled “1. Create the instance”Create the instance once, at module level, in a file your components import.
Pass each provider its ID from the environment. This page assumes SvelteKit 3,
which declares environment variables in src/env.ts; a variable with
public: true can be imported in browser code from $app/env/public:
import { defineEnvVars } from '@sveltejs/kit/env'
export const variables = defineEnvVars({ GA4_MEASUREMENT_ID: { public: true, schema: (value) => value ?? '' }, FACEBOOK_PIXEL_ID: { public: true, schema: (value) => value ?? '' },})import { FACEBOOK_PIXEL_ID, GA4_MEASUREMENT_ID } from '$app/env/public'import { createAnalytics } from 'consentlayer'import { ga4 } from 'consentlayer/providers/ga4'import { meta } from 'consentlayer/providers/meta'import { autoTracking } from 'consentlayer/auto-tracking'
export const analytics = createAnalytics({ providers: [ga4({ measurementId: GA4_MEASUREMENT_ID }), meta({ pixelId: FACEBOOK_PIXEL_ID })], autoTracking: autoTracking(),})A schema function receives the raw value, undefined when the variable is
not set, and returns the value your code imports. Without a schema, SvelteKit
refuses to start when the variable is missing.
The options are the same as in React; React
explains how missing IDs, autoTracking, enabled and debug behave.
2. Call consentLayer in the root layout
Section titled “2. Call consentLayer in the root layout”<script lang="ts"> import { consentLayer } from 'consentlayer/sveltekit' import { analytics } from '#lib/analytics.ts' import ConsentBanner from '#lib/ConsentBanner.svelte'
let { children } = $props() const consent = consentLayer({ analytics })</script>
{@render children()}<ConsentBanner privacyPolicyUrl="/privacy" />{#if consent.ready && !consent.bannerVisible} <button type="button" onclick={consent.openDialog}>Cookie settings</button>{/if}#lib is SvelteKit 3’s alias for src/lib, which replaced $lib. It is a
Node subpath import that you declare in the imports field of package.json,
as new projects from sv already do, so imports from it include the file
extension:
{ "imports": { "#lib/*": "./src/lib/*" }}consentLayer() sets a context, so call it while the component initializes,
in its <script>, not in a callback. Once the layout is mounted in the
browser, it performs these steps:
- It calls
analytics.consent.bootstrap(), which reads the stored choice fromlocalStorage, or from your consent adapters if nothing is stored. When neither yields a choice, or the stored choice is stale,bannerVisiblebecomestrue, unlessautoShowBannerisfalse. - It schedules
analytics.boot()according to thebootoption. - It sends a page view to every active provider. A provider that becomes active later, because the visitor grants its category, receives the current page view at that point.
- Through SvelteKit’s
afterNavigate, after every navigation to a new path or query string, it sends a page view and callsanalytics.newPageSession(). A change to the hash alone sends nothing.
When the layout is destroyed, it stops. On the server it does nothing, and
ready stays false, so the banner is not rendered there and the HTML matches
the first render in the browser.
consentLayer() returns the same object as useConsentController(). Its
state is reactive properties, so read consent.bannerVisible directly;
destructuring it reads the value once.
| Option | Type | Default | Description |
|---|---|---|---|
analytics |
Analytics |
Required | The instance from createAnalytics(). |
page |
{ url?, path?, title? } |
location and document.title |
Overrides the page view’s details. |
boot |
{ mode?, timeoutMs? } |
{ mode: 'immediate' } |
When providers start. |
autoShowBanner |
boolean |
true |
Whether bannerVisible becomes true when there is no stored choice or the stored choice is stale. |
openConsentGlobalKey |
string |
'CONSENTLAYER_openConsent' |
The window function that opens the settings dialog from outside Svelte. |
3. Add the banner
Section titled “3. Add the banner”Copy the Svelte consent banner recipe into
src/lib/ConsentBanner.svelte and render it once, in the root layout as above.
In other components, useConsentController() returns the object
consentLayer() returned; it throws when no parent component called
consentLayer().
Meta Pixel and GTM without JavaScript
<MetaPixelNoscript> renders Meta’s <noscript> PageView image,
https://www.facebook.com/tr?id=<pixelId>&ev=PageView&noscript=1, for visitors
with JavaScript disabled:
<script lang="ts"> import { FACEBOOK_PIXEL_ID } from '$app/env/public' import { MetaPixelNoscript } from 'consentlayer/sveltekit'</script>
<MetaPixelNoscript pixelId={FACEBOOK_PIXEL_ID} enabled />Those visitors never see the consent banner, so the image tracks them without
consent. For that reason it renders nothing unless you pass enabled, and
nothing while pixelId is undefined or empty. Other attributes are passed to
the image. It only has an effect in HTML rendered on the server, as with
SvelteKit or render from svelte/server; an application rendered only in the
browser never shows it. The component ships as Svelte source, which your Svelte
compiler builds with your application.
<GtmNoscript containerId={…} enabled /> does the
same for Google Tag Manager’s <noscript> iframe; see
Google Tag Manager.
4. Track events
Section titled “4. Track events”useAnalytics() returns the instance passed to consentLayer(), or the most
recently created instance when it was not called. Call it while a component
initializes. Outside components, import the instance from
src/lib/analytics.ts.
<script lang="ts"> import { useAnalytics } from 'consentlayer/sveltekit'
const analytics = useAnalytics()</script>
<button onclick={() => analytics.track('sign_up', { method: 'email' })}>Sign up</button>useConsent() returns { consent, engine, setConsent, update }. consent is a
reactive property holding the current consent, null before a choice, and it
updates when consent changes. Read it in the template or in $derived or
$effect; Svelte listens for changes only while something reads it. engine
is analytics.consent; setConsent(consent) saves a complete choice and
update(partial) merges a partial one into the current consent.
Events and identity lists the standard events and what each provider receives.
5. Report errors
Section titled “5. Report errors”Every error during loading, rendering and navigation in the browser reaches
SvelteKit’s client handleError hook. handleErrorWith() returns a hook that
sends the ones your code threw:
import { handleErrorWith } from 'consentlayer/sveltekit'import { analytics } from '#lib/analytics.ts'
export const handleError = handleErrorWith(analytics)It calls captureError(error, { source: 'boundary' }) when kind is
'unknown', for errors thrown by your code or code it calls. Errors from
error(), 'app', and those SvelteKit generates, such as 404s,
'framework', are expected, so it skips them. It returns nothing, so SvelteKit
shows its default message. To return your own, call captureError in your own
hook instead.
As in React, captureError reaches the providers that implement error
reporting, Sentry and PostHog, and the window’s error and
unhandledrejection events are reported after boot unless you set
errors: { globalHandlers: false }.
Without SvelteKit
Section titled “Without SvelteKit”In a Svelte application without SvelteKit, import everything from
consentlayer/svelte instead, and call consentLayer() in your root
component. With Vite, the provider IDs come from import.meta.env, such as
import.meta.env.VITE_GA4_MEASUREMENT_ID.
<script lang="ts"> import { consentLayer } from 'consentlayer/svelte' import { analytics } from './lib/analytics' import ConsentBanner from './lib/ConsentBanner.svelte' import Page from './Page.svelte'
const consent = consentLayer({ analytics })</script>
<Page /><ConsentBanner privacyPolicyUrl="/privacy" />{#if consent.ready && !consent.bannerVisible} <button type="button" onclick={consent.openDialog}>Cookie settings</button>{/if}It sends one page view for the page load. With a client-side router, pass an
afterNavigate option: a function that registers a callback and calls it after
every navigation with { to: { url } }, where url is the new URL.
ConsentLayer sends a page view and calls analytics.newPageSession() when the
path or query string changes; a change to the hash alone sends nothing. It must
call the callback once for the first page too.
handleErrorWith() is not available. Wrap your components in
<svelte:boundary> and report from its onerror:
<svelte:boundary onerror={(error) => analytics.captureError(error, { source: 'boundary' })}> <Page /></svelte:boundary>The SvelteKit example in the repository runs the setup on this page.