---
title: Quickstart
description: Add c15t to a Vue 3 app built with Vite, using the Vue plugin,
  Inth, consent-gated scripts and a preferences link.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## Before you start

This guide is for a Vue 3 single-page app built with Vite. The build bundles
your project's public policy, and the browser resolves each visitor's policy
from it after the page loads, so it works on any static host. For a Vue app that renders on the server, use the c15t Nuxt module.
[Choose your setup](/docs/concepts/choose-your-setup) compares every option.

The guide uses [Inth](https://inth.com) for policies and consent records.
Create a project, set its policy rules, add your site's origin to its trusted
origins, and copy the backend URL. To run the service yourself, use a
[self-hosted backend](/docs/self-host/overview) URL instead. Offline mode keeps
policies in your code and choices in the browser, with no consent records.
Not recommended for production environments.

Bundling the policy at build time is the recommended production setup.
Rebuild after changing policies, translations or vendors.

## Install c15t

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

## Add the Vite plugin

Add `consentManifest()` from `c15t/vue/vite` to `vite.config.ts`, next to
`@vitejs/plugin-vue`:

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

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

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

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

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 `/manifest`
during `vite build`, or in `vite dev` when the app first loads it, and hands
it and the backend URL to the Vue plugin. A build fetches only when the app
uses `manifest()`. No file is written into your project. It also keeps the c15t
components, which ship as `.vue` files, out of Vite's dependency
pre-bundling.

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.

## Install the Vue plugin

Install the plugin from `c15t/vue/vue-plugin` in `src/main.ts`:

```ts title="src/main.ts"
import { posthog } from '@c15t/integrations/posthog';
import { c15tVue, manifest } from 'c15t/vue/vue-plugin';
import { createApp } from 'vue';

import App from './App.vue';

createApp(App)
	.use(c15tVue, {
		mode: manifest(),
		scripts: [
			posthog({
				id: 'phc_your_project_key',
				initOptions: { cookieless_mode: 'never' },
				loadMode: 'after-consent',
			}),
		],
	})
	.mount('#app');
```

`c15t/vue` is the Nuxt module. Do not pass it to `app.use()`.

The plugin creates the consent runtime for this app. When the app mounts, it
reads the visitor's stored choice, resolves their policy, and starts the
script loader for the `scripts` you pass. `mode: manifest()` resolves the
policy in the browser from the snapshot `consentManifest()` downloaded, with
no `/manifest` or `/init` request, and sends consent choices to the same
backend URL. Vite reads `VITE_C15T_BACKEND_URL` at build time, so set it
before you build, not when you serve the files.

The browser does not know the visitor's location, so without `inputs` or
`geoURL` on `manifest()`, or a `country` and `region` passed to
`ConsentRoot`, every visitor gets the policy your project assigns when the
location is unknown. Configure that fallback rule in your project, or see
[rendering](/docs/frameworks/vue/rendering#tell-the-browser-where-the-visitor-is).
The bundle carries English copy; other languages load on demand.

To apply policy edits without rebuilding, use `manifest({ source: 'runtime' })`
or `hosted()`. [Rendering](/docs/frameworks/vue/rendering) compares the
modes.

## Register consent-gated scripts

Remove any existing PostHog snippet so the script loads once, and add
measurement to your Inth policy. The `scripts` option in `src/main.ts` loads
PostHog once the visitor allows measurement.

Replace `phc_your_project_key` with your PostHog project key. PostHog
defaults to its EU region; set `region: 'us'` for a US project.

## Mount the consent UI

```vue title="src/App.vue"
<script setup lang="ts">
import { ConsentDialogLink, ConsentRoot } from 'c15t/vue/vue-plugin';
</script>

<template>
	<ConsentRoot />
	<main>
		<h1>c15t with Vue</h1>
		<p>PostHog loads after you allow measurement.</p>
	</main>
	<footer>
		<ConsentDialogLink>Privacy settings</ConsentDialogLink>
	</footer>
</template>
```

`ConsentRoot` renders the banner when the visitor's policy asks for one and
loads the preference dialog when it opens. `ConsentDialogLink` renders a
button that reopens preferences. Without slot content, its label is the
dialog's title in the visitor's language: Privacy Settings in English. It
renders nothing until the policy offers preferences. Replace the `<main>`
element with your own content or router view. Keep `ConsentRoot` in the root
component so it stays mounted. The components bring their own styles; there
is no stylesheet to import.

## Gate an embed

Wrap third-party iframes in `ConsentGate`. The iframe does not exist in the
page until its category is allowed, and it is removed when the visitor
withdraws permission.

```vue title="src/VideoEmbed.vue"
<script setup lang="ts">
import { ConsentDialogLink, ConsentGate } from 'c15t/vue/vue-plugin';
</script>

<template>
	<ConsentGate category="measurement">
		<iframe
			src="https://www.youtube-nocookie.com/embed/czTksCF6X8Y"
			title="YouTube video"
			loading="lazy"
			allowfullscreen
		/>
		<template #placeholder>
			<p>Allow measurement to load this YouTube video.</p>
			<ConsentDialogLink>Choose video permissions</ConsentDialogLink>
		</template>
	</ConsentGate>
</template>
```

Render `<VideoEmbed />` in a page where the video belongs.

## Verify the setup

Build and preview the production app with `vite build` and `vite preview`.
Open it in a private window with your browser's DevTools on the Network tab,
under a policy that asks for consent.

1. Load the page. The bundled manifest resolves the policy and the banner
   appears, with no `/manifest` or `/init` request. Test the unknown-location
   rule, or pass `geoURL` to `manifest()` if you need regional policies. No requests to
   `posthog.com` or `youtube-nocookie.com` appear.
2. Click **Reject All**, then reload. The banner stays closed and the vendor
   requests stay absent.
3. Click **Privacy settings**, turn on **Analytics** (the `measurement` category) and save. PostHog's `array.js`
   loads and the video appears.
4. Turn **Analytics** off and save. The page reloads, the video is gone and
   PostHog does not load again.

[Verify consent](/docs/guides/verify-consent) has the full release checklist.

## Next steps

* [Rendering and deployment](/docs/frameworks/vue/rendering) covers static
  hosting, each `mode` and server rendering.
* [c15tVue plugin](/docs/frameworks/vue/plugin) lists every option.
* [Scripts](/docs/frameworks/vue/scripts), [embeds](/docs/frameworks/vue/embeds)
  and the [network blocker](/docs/frameworks/vue/network-blocker) cover
  gating and revocation.
* [Customize](/docs/frameworks/vue/customize) changes colors, layout and copy.
* [Components](/docs/frameworks/vue/components) and
  [composables](/docs/frameworks/vue/composables) list the full API.
* [Troubleshooting](/docs/frameworks/vue/troubleshooting) covers common
  failures.
