---
title: Quickstart
description: Add a c15t cookie banner, preference dialog and consent-gated
  scripts to a Svelte 5 app built with Vite, using Inth as the consent backend.
group: frameworks
lastModified: "2026-10-29T15:17:56Z"
lastAuthor: Christopher Burns
---
## Before you start

This guide is for a Svelte 5.20 or later app without SvelteKit, such as a Vite
single-page app. The browser resolves consent after the page loads, so the
banner appears after the first paint. For a banner in the server HTML, use
SvelteKit; [choose your setup](/docs/concepts/choose-your-setup) compares the
options. The `examples/svelte` app in the c15t repository is the finished
result.

The guide uses [Inth](https://inth.com) for policies and consent records.
[Create an Inth project](https://inth.com/dashboard/sign-up?framework=svelte), then:

1. Set its policy rules, including the `measurement` category for the
   vendor below.
2. Add your app's origin, such as `http://localhost:5173`, to its trusted
   origins.
3. Copy the project's backend URL.

A self-hosted backend or local mode works too; see
[other setups](#other-setups).

The guide uses `hosted()` mode. The browser asks your backend for each
visitor's policy, and the backend reads the visitor's location from the
request, so regional policies need no extra setup. Policy, translation and
vendor edits reach visitors on their next page load, without a rebuild.

## Install

|Package manager|Command|
|:--|:--|
|npm|`npm install c15t @c15t/integrations`|
|pnpm|`pnpm add c15t @c15t/integrations`|
|yarn|`yarn add c15t @c15t/integrations`|
|bun|`bun add c15t @c15t/integrations`|

The `c15t` package exports the Svelte components and consent modes from
`c15t/svelte`. `@c15t/integrations` holds the vendor helpers.

## Add the Vite plugin

Add `consentManifest` to the plugins in `vite.config.ts`:

```ts title="vite.config.ts"
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { consentManifest } from 'c15t/svelte/vite';
import { defineConfig } from 'vite';

export default defineConfig({ plugins: [consentManifest(), svelte()] });
```

Set `VITE_C15T_BACKEND_URL` to your project's backend URL, including any path
prefix, in `.env` or your build environment:

```sh title=".env"
VITE_C15T_BACKEND_URL=https://your-project.inth.app
```

Plain Svelte apps (with Vite) use `VITE_C15T_BACKEND_URL`. SvelteKit apps use
`PUBLIC_C15T_BACKEND_URL` instead. If your app already sets `VITE_INTH_PROJECT_URL`
or `PUBLIC_INTH_PROJECT_URL` for other Inth SDKs, those work too. When both are
set, the c15t variable wins.

`consentManifest` hands the backend URL to `hosted()`. No file is written
into your project. Without the plugin, pass the URL as
`hosted({ backendURL })`.

The plugin also adds a small script to `index.html` that sends `/init` while
your JavaScript downloads, so the banner shows sooner.
[Send `/init` early](#send-init-early) covers when to turn it off.

## Mount the provider

Render `ConsentProvider` around your app in `src/App.svelte`, with the
vendor scripts c15t should load:

```svelte title="src/App.svelte"
<script lang="ts">
	import { posthog } from '@c15t/integrations/posthog';
	import {
		ConsentBanner,
		ConsentDialog,
		ConsentDialogLink,
		ConsentProvider,
		hosted,
	} from 'c15t/svelte';

	const scripts = [
		posthog({
			id: 'phc_your_project_key',
			initOptions: { cookieless_mode: 'never' },
			loadMode: 'after-consent',
		}),
	];
</script>

<ConsentProvider mode={hosted()} {scripts}>
	<main>
		<h1>c15t + Svelte</h1>
		<p>Your app goes here.</p>
	</main>
	<footer>
		<ConsentDialogLink>Privacy settings</ConsentDialogLink>
	</footer>
	<ConsentBanner />
	<ConsentDialog />
</ConsentProvider>
```

The `<main>` element stands in for your app's content. `hosted()` from
`c15t/svelte` makes one `/init` request to the backend URL from
`VITE_C15T_BACKEND_URL`, started early by the script in `index.html`. The
backend picks the policy from the visitor's location and language and returns
its current version. Consent choices go to the same backend. Vite writes the
variable into the bundle at build time, so changing it means a rebuild.

PostHog waits for measurement permission. Replace `phc_your_project_key`
with your PostHog project key. Remove any `<script>` tags or SDK imports that
already load PostHog, so c15t is the only thing that loads it.

The provider starts consent when it mounts and stops it when it unmounts, so
keep it at the root of the app.

`ConsentBanner` shows the banner when the visitor's policy asks for one.
`ConsentDialog` is the preference dialog; it loads in its own chunk after the
page, before anyone opens it. `ConsentDialogLink` is the persistent way back to
preferences. Put it where visitors look for privacy settings, such as the
footer.

There is no stylesheet to import. Each component adds the rules it uses to
`<head>` when it first renders. With Tailwind CSS 3, import
`c15t/svelte/styles.css` yourself and pass `styles={false}` to the provider;
see [stylesheets and CSS layers](/docs/customization/stylesheets#import-the-stylesheet-yourself).

## Verify consent

Build and preview the production bundle with `vite build` and `vite preview`,
then open the app in a private window with DevTools open on the Network tab:

1. One `/init` request goes to your backend URL, starting before your
   JavaScript finishes downloading, and the banner appears. No requests go to
   PostHog yet.
2. Select **Reject All**. Reload. The banner stays closed and PostHog requests
   stay absent.
3. Select **Privacy settings**, turn on **Analytics** (the `measurement`
   category) and save. PostHog loads.
4. Turn it off and save. The page reloads and PostHog does not load.

If the banner does not appear, check the `/init` request, then see
[troubleshooting](/docs/frameworks/svelte/troubleshooting).
[Verify consent](/docs/guides/verify-consent) has the release checklist.

## Send `/init` early

`consentManifest()` adds a small inline script to the `<head>` of
`index.html`. It sends `/init` while the browser downloads your JavaScript,
and `hosted()` uses that response instead of sending its own. The banner
usually shows 50 to 250 ms sooner. There is nothing to configure.

The early request can't read your app's options, so in a few setups it won't
match and the app sends a second `/init`. Pass `initPrefetch: false` when the
app uses `local()` or gives `hosted()` its own `backendURL`, `fetch`,
`initURL` or `headers`. With its own `fetch`, `hosted()` sends `/init` through
it and ignores the early response. If it sets `credentials`, `overrides`, `storageKey`, an
`experiment`, or `journey: false`, pass the same values as
`initPrefetch: { credentials, experiment, journey, overrides, storageKey }`.
With `journey: 'tab'`, passing it too makes a tab's first `/init` report the
`'tab'` scope instead of `'page'`. It doesn't save a request.
Pass `experiment` only when c15t assigns the arm. If a feature flag picks
it, leave it out, or the script sends an arm the visitor may never see.
The dev server adds the script even to a `manifest()` app, which then sends
one `/init` per page load that it doesn't use. A production build leaves it
out.

## Other setups

**Local mode** resolves policies in the browser and keeps choices in the
browser's cookie and local storage, with no backend and no consent records. Use it for local development
and tests. Not recommended for production environments.

```ts
import { local } from 'c15t/svelte';

const mode = local();
```

With no arguments, `local()` uses the recommended rules. They ask for
opt-in in Europe and unknown locations, use opt-out in US privacy states, and
show no banner where no consent law applies. Pass `local({ policyRules })` to use your own.
[Data fetching](/docs/concepts/data-fetching#when-should-i-use-local-mode)
explains the trade-offs.

**A self-hosted backend** uses the same setup with your own backend URL. See
[self-hosting](/docs/self-host/overview).

**A bundled policy** with `manifest()` saves the `/init` request in a few
cases; see [bundle the policy at build time](#bundle-the-policy-at-build-time).

**Astro** can render these Svelte components as islands. Its integration uses
Svelte for the dialog by default. Pick the Astro row in
[choose your setup](/docs/concepts/choose-your-setup).

Next, [customize](/docs/frameworks/svelte/customize) the banner or read
consent in your own components with [context getters](/docs/frameworks/svelte/getters).

## Bundle the policy at build time

`manifest()` resolves the policy in the browser from a snapshot
`consentManifest` downloads during `vite build`. It saves the `/init` request
only in these cases:

* The policy has no location rules, or every location gets the same banner.
* The page already knows the visitor's location, for example from an edge
  worker, and passes it as `manifest({ inputs: { country, region } })`.
* You run a same-origin route that returns the location and set it as
  `geoURL`.

Otherwise the browser still calls `/init` on a first visit and uses the
backend's answer, so the bundle adds bytes without saving a request.
`consentManifest` warns during the build when that happens. The resolver adds
about 3 KB gzipped, loaded as its own chunk, and the backend doesn't count
visitors the browser resolves this way. Swap the mode in `src/App.svelte`:

```svelte title="src/App.svelte (partial)"
<script lang="ts">
	import { ConsentProvider, manifest } from 'c15t/svelte';
</script>

<ConsentProvider mode={manifest()} {scripts}>
```

The bundle carries the policy and English copy. A visitor who resolves to
another language loads that language's copy the first time it is needed.

The snapshot is fixed at build time:

* Rebuild after changing policies, translations or vendors. If your CI caches
  build output, force a fresh build.
* Consent choices still go to the backend.

The build reads the backend URL from your public backend URL variable when
the config doesn't pass one: `NEXT_PUBLIC_C15T_BACKEND_URL` in Next.js,
`NUXT_PUBLIC_C15T_BACKEND_URL` in Nuxt, `PUBLIC_C15T_BACKEND_URL` in Astro,
Svelte and SvelteKit, and `VITE_C15T_BACKEND_URL` in TanStack Start and other
Vite apps. Each also reads the matching Inth variable, such as
`NEXT_PUBLIC_INTH_PROJECT_URL`, when the c15t one is unset. See
[set the backend URL](/docs/concepts/modes#set-the-backend-url).

The fetch waits at most 10 seconds. When it fails, or no backend URL is set,
every framework does the same thing:

|Command|Default when the fetch fails|
|--|--|
|Production build: `next build`, `vite build`, `nuxt build`, `astro build`|The build stops with an error.|
|Dev: `next dev`, `vite dev`, `nuxt dev`, `astro dev`|A warning, and the server fetches the policy at runtime.|

Set `onBuildError` to use one behaviour for both. `'fail'` stops dev too.
`'runtime'` lets a production build finish, and the server fetches the policy
at runtime. The `C15T_ON_BUILD_ERROR` environment variable overrides the
option, so you can deploy during a backend outage without a code change:

```sh
C15T_ON_BUILD_ERROR=runtime npm run build
```

Turborepo's strict environment mode hides undeclared variables from tasks, so
list `C15T_ON_BUILD_ERROR` in the build task's `passThroughEnv` there.

The build skips the fetch, without an error, when it can't use a snapshot,
for example when the backend URL is relative. With `onBuildError: 'fail'`, a
relative URL stops the build. [Consent modes](/docs/concepts/modes#what-happens-when-the-download-fails)
lists every case.

`vite build` and `vite dev` fetch the manifest when they start. `vite preview`
serves the last build without fetching. The plugin writes no file into your
app, so there is nothing to keep out of Git. `c15t/generated` ships its own
types, so `tsc`, `vue-tsc` and `svelte-check` pass on a fresh checkout without
a build first.

The plugin can't see the options your app passes to `manifest()`. When the
app passes `source: 'runtime'` or `manifestURL`, set
`consentManifest({ source: 'runtime' })` too. The build then downloads no
manifest, so it doesn't fail when the backend's `/manifest` is down.

`manifest({ source: 'runtime' })` fetches the manifest from the backend when
the page loads, so policy edits apply without a rebuild.
`manifest({ manifestURL })` fetches it from another URL, such as a CDN,
instead of using the build's snapshot.
