Svelte
Quickstart
Before you start
This guide is for a Svelte 5 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 compares the
options. The examples/svelte app in the c15t repository is the finished
result.
The guide uses Inth for policies and consent records. Create an Inth project, then:
- Set its policy rules, including the
measurementcategory for the vendor below. - Add your app's origin, such as
http://localhost:5173, to its trusted origins. - Copy the project's backend URL.
A self-hosted backend or offline mode works too; see other setups.
The setup bundles your policy into the browser bundle at build time, the recommended production setup. Rebuild after changing policies, translations or vendors.
Install
Svelte uses its own package, @c15t/svelte, which exports the components and
the manifest(), hosted() and offline() modes. @c15t/integrations
holds the vendor helpers. The c15t package has no Svelte entry point, so
you don't install it.
Bundle the policy at build time
Add consentManifest to the plugins in vite.config.ts:
Set VITE_C15T_BACKEND_URL to your project's backend URL, including any path
prefix, in .env or your build environment:
If your app already sets VITE_INTH_PROJECT_URL for other Inth SDKs, that works
too. When both are set, VITE_C15T_BACKEND_URL wins.
consentManifest downloads your project's public policy from
<backendURL>/manifest during vite build, or in vite dev when the app
first loads it, and hands it and the backend URL to manifest(). A build
fetches only when the app uses manifest(). No file is written into your project.
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.
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:
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
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.
Mount the provider
Render ConsentProvider around your app in src/App.svelte, with the
vendor scripts c15t should load:
The <main> element stands in for your app's content. manifest() from
@c15t/svelte, with no options, resolves the policy from the snapshot
consentManifest downloaded and saves choices to the same
VITE_C15T_BACKEND_URL. 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 browser resolves the snapshot without a request when the policy does not
depend on geography, or when it knows the country and region the policy needs.
Pass them as manifest({ inputs: { country, region } }), for example from an
edge worker, or set geoURL to a same-origin route that returns them, to keep
regional resolution on the snapshot. Otherwise it calls the backend's /init
and uses its resolved policy and translations. Those visitors wait for a
backend round trip and can receive newer policy than the build snapshot. The
bundle carries the policy and English copy. The resolver loads as its own
chunk when the app starts, and a visitor who resolves to another language
loads that language's copy the first time it is needed.
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.
To apply policy edits without a rebuild, pass manifest({ source: 'runtime' }),
which fetches the manifest from the backend when the page loads.
manifest({ manifestURL }) fetches it from another URL, such as a CDN,
instead of using the build's snapshot. To have the backend resolve every
visitor's location, pass hosted(). Like manifest(), it reads the backend
URL from consentManifest, and a hosted() build never downloads the
manifest:
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:
- The banner appears. No requests go to PostHog yet.
- Select Reject All. Reload. The banner stays closed and PostHog requests stay absent.
- Select Privacy settings, turn on Analytics (the
measurementcategory) and save. PostHog loads. - Turn it off and save. The page reloads and PostHog does not load.
If the banner does not appear, see troubleshooting. Verify consent has the release checklist.
Other setups
Offline 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.
With no arguments, offline() 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 offline({ policyRules }) to use your own.
Data fetching
explains the trade-offs.
A self-hosted backend uses the same setup with your own backend URL. See self-hosting.
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.
Next, customize the banner or read consent in your own components with context getters.