[Consent banner](/consent-banner)

# Add a cookie consent banner to a JavaScript app

c15t puts the banner in your JavaScript app as code, with no UI framework. It holds tags until the visitor chooses and keeps Google Consent Mode v2 in sync.

[Start for free](/dashboard/sign-up) · [See the code](#install)

Ask your coding agent: `Add inth to my app: inth.com/SKILL.md`

- **Framework**: JavaScript apps built with Vite
- **Library**: @c15t/browser, open source
- **Code checked**: 28 October 2026

## Install c15t in a JavaScript app

c15t is the open source consent library behind Inth Consent. It renders the banner from your own bundle and records each choice in your Inth project.

```bash
npm install @c15t/browser c15t @c15t/integrations
```

`@c15t/browser` is not part of the `c15t` package. `c15t` gives you the Vite plugin, and `@c15t/integrations` gives you the vendor helpers.

Add your Inth project URL to `.env`. Copy it from your Inth dashboard. Vite reads it at build time, so set it before you build.

```bash
VITE_C15T_BACKEND_URL=https://your-project.inth.app
```

Add `consentManifest` to `vite.config.ts`. It downloads your policy during `vite build`. Rebuild after you change a policy.

```ts
import { consentManifest } from 'c15t/build';
import { defineConfig } from 'vite';

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

Code on this page comes from the [c15t JavaScript quickstart](https://c15t.com/docs/frameworks/javascript/quickstart), checked 28 October 2026.

## Start c15t

Call `init()` one time in your browser entry point. It reads stored choices, resolves the policy for the visitor and shows the banner when the policy needs one.

```ts
import { init, manifest } from '@c15t/browser';
import { posthog } from '@c15t/integrations/posthog';

init({
  mode: manifest(),
  scripts: [
    posthog({
      id: 'phc_your_project_key',
      initOptions: { cookieless_mode: 'never' },
      loadMode: 'after-consent',
    }),
  ],
});
```

The banner renders in a shadow root with its own styles, so you import no stylesheet. To apply policy edits without a rebuild, give `hosted()` from `@c15t/browser` as the mode instead.

## Hold scripts until consent

With `loadMode: 'after-consent'`, PostHog waits for measurement permission. Remove every other loader for PostHog, so it loads once and only through c15t.

[JavaScript scripts and embeds](https://c15t.com/docs/frameworks/javascript/scripts) covers more vendors, iframes and the network blocker. The [c15t integrations](https://c15t.com/docs/integrations/overview) list the helper for each vendor.

## Hold embeds and add a privacy settings link

Move the URL of an iframe from `src` to `data-src` and add `data-category`. c15t sets `src` after the visitor gives consent for that category.

```html
<iframe
  data-src="https://www.youtube-nocookie.com/embed/czTksCF6X8Y?playsinline=1"
  data-category="measurement"
  title="YouTube video"
  allow="encrypted-media; picture-in-picture"
  allowfullscreen
></iframe>
```

A link to `#c15t-preferences` opens the preference dialog. Put it in the footer of every page.

```html
<a href="#c15t-preferences">Privacy settings</a>
```

## Build your own banner UI

To render your own banner, use the [headless runtime](https://c15t.com/docs/frameworks/javascript/headless). The client also lets your code read consent. `consent.has('measurement')` tells you if a category is allowed now, and `consent.on('consent', listener)` reports changes.

To change the stock banner instead, set `ui.theme`. [Customize](https://c15t.com/docs/frameworks/javascript/customize) covers each option.

## Google Consent Mode v2 in JavaScript

The Google tag helper sends the Consent Mode v2 default before your config and an update after each choice. It ships in `@c15t/integrations`.

```ts
import { gtag } from '@c15t/integrations/google-tag';

export const scripts = [gtag({ id: 'G-XXXXXXXXXX', category: 'measurement' })];
```

Add the helper to the `scripts` array that you give to `init()`, next to PostHog.

Measurement maps to `analytics_storage`. Marketing maps to `ad_storage`, `ad_user_data` and `ad_personalization`.

By default the helper loads gtag.js before a choice. Google then gets a default that matches the current permissions. Set `loadMode: 'after-consent'` to send nothing to Google until the visitor accepts.

[Read the developer guide to Consent Mode v2](/blog/developer-guide-to-consent-mode-v2).

## Show the right banner in each region

Your Inth project sets a policy for each region, such as opt in for the EU and UK or opt out for California. `manifest()` resolves the policy in the browser from the bundled snapshot. When the policy depends on the country or region of the visitor, the client also sends an `/init` request to your backend.

Test each region before launch. [See how regional policies work](/consent-banner/regions).

## Check that consent gates work

Build with `vite build`, run `vite preview` and open a private window with the Network tab open. Test under an opt in policy, such as the EU policy.

1. Under an opt in policy, no PostHog request leaves the page before a choice.
2. After Reject All and a reload, PostHog stays off.
3. When you turn on measurement in Privacy settings, PostHog loads.
4. When you turn measurement off, the page reloads without PostHog.

Choices post to your Inth project. Check that Inth has a record for each one. The [c15t verify consent guide](https://c15t.com/docs/guides/verify-consent) has the full release checklist.

## Use Inth in production

c15t is open source. Point it at your Inth project, which hosts the backend and keeps the consent records. [See Inth open source](/oss).

c15t also has a local mode with no backend. It keeps no consent records, so the docs advise against it in production.

Want to see what else your repo sends before consent? [Run a free codebase scan](/run-free-codebase-scan).

## Cookie consent banners for other frameworks

The same banner and the same Inth project work in other stacks. See the setup for [Next.js](/consent-banner/nextjs), [React](/consent-banner/react), [TanStack Start](/consent-banner/tanstack-start), [Nuxt](/consent-banner/nuxt), [Vue](/consent-banner/vue), [Astro](/consent-banner/astro), [Svelte](/consent-banner/svelte), [SvelteKit](/consent-banner/sveltekit) and [HTML](/consent-banner/html).

No bundler? See the [HTML cookie consent banner](/consent-banner/html). It needs one script tag.

[See how the Inth consent banner works](/consent-banner).

## Questions about JavaScript consent

### Can I build my own banner UI?

Yes. Use the [headless runtime](https://c15t.com/docs/frameworks/javascript/headless) and read consent with `consent.has()` and `consent.on()`.

### Do I need a UI framework?

No. `@c15t/browser` renders the stock banner in a shadow root with its own styles.

### What if my site has no bundler?

Use the script tag setup. See the [HTML cookie consent banner](/consent-banner/html).

### What happens when a visitor withdraws consent?

The client saves the choice and reloads the page, because the browser cannot unload code that already ran.

### How do I set Consent Mode v2 defaults before gtag loads?

Use the c15t Google tag helper. It sends the default before your config and an update after each choice.

### Does it show a different banner in each region?

Yes. Set a policy for each region in your Inth project. c15t applies the policy that matches the visitor’s location. [See regional policies](/consent-banner/regions).

### Can I run it without a backend?

c15t has a local mode with no backend, but it keeps no consent records. Use your Inth project in production.