---
title: Quickstart
description: Add a consent banner, preferences dialog and consent-gated scripts
  to a React single-page app built with Vite, using Inth as the consent backend.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## Before you start

This guide sets up a client-rendered React app built with Vite. The build
bundles your project's public policy. The browser loads the page, resolves the
visitor's policy from that bundle, then shows the banner or runs the scripts
the visitor allowed. Regional policies may still need a backend location
request.

* **Next.js or TanStack Start?** Use that framework's guide from the
  [framework list](/docs/frameworks). Their adapters resolve consent on the
  server.
* **React Router framework mode, Remix or another server-rendered React app?**
  This guide still works. The server renders the page without consent state and
  the browser resolves it after hydration. Read
  [rendering](/docs/frameworks/react/rendering#react-router-remix-and-other-server-rendered-apps)
  for what that means for the first paint.

You need an [Inth](https://inth.com) project. In the project, set the policy
rules for your regions, add your site's origin to the trusted origins, and copy
the backend URL.
[Choose your setup](/docs/concepts/choose-your-setup#decide-who-runs-the-consent-backend)
covers self-hosting and offline mode.

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`|

`c15t` contains the React adapter at `c15t/react` and the Vite plugin at
`c15t/build`. `@c15t/integrations` holds the vendor loaders used below.

## Bundle your policy

Add `consentManifest` to `vite.config.ts`:

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

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

The plugin reads `VITE_C15T_BACKEND_URL`. Set it to your project's backend URL,
exactly as Inth shows it, 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 serves
it to your app as `snapshot`
from `c15t/generated`. 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](/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.

## Mount the provider

Create `src/consent.tsx`. `ConsentProvider` creates the consent runtime and
loads the scripts. `ConsentBanner` and `ConsentDialog` render when the policy
asks for them, and `ConsentDialogLink` keeps preferences reachable after the
banner closes.

```tsx title="src/consent.tsx"
import { posthog } from '@c15t/integrations/posthog';
import {
	ConsentBanner,
	ConsentDialog,
	ConsentDialogLink,
	ConsentProvider,
	manifest,
} from 'c15t/react';
import type { ReactNode } from 'react';

const options = {
	// The policy the build downloaded from VITE_C15T_BACKEND_URL.
	mode: manifest(),
	scripts: [
		posthog({
			id: 'phc_your_project_key',
			initOptions: { cookieless_mode: 'never' },
			loadMode: 'after-consent',
		}),
	],
};

export const Consent = ({ children }: { children: ReactNode }) => (
	<ConsentProvider options={options}>
		{children}
		<ConsentBanner />
		<ConsentDialog />
		<footer>
			<ConsentDialogLink>Privacy settings</ConsentDialogLink>
		</footer>
	</ConsentProvider>
);
```

`manifest()` resolves the policy from the bundled snapshot in the browser,
with no `/manifest` request. It reads the snapshot and the backend URL from
what `consentManifest` downloaded. Consent choices go to that backend, and so
does an `/init` request when the policy depends on the visitor's country or
region and the browser does not know it. That request returns the backend's
current policy, which can be newer than the snapshot. Vite reads
`VITE_C15T_BACKEND_URL` at build time, so set it before you build, not when
you serve the files.

To apply policy edits without rebuilding, use `hosted()` from `c15t/react` as
the mode. The browser then requests the policy from `/init` on every page load.
[Consent modes](/docs/concepts/modes) compares the three modes.

PostHog waits for measurement permission, so your policy must include that
category. Replace `phc_your_project_key` with your PostHog project key.
PostHog uses its EU host; add `region: 'us'` for a US project. Remove any
other PostHog loader from your app so PostHog loads once.

`loadMode: 'after-consent'` keeps the PostHog SDK off the page until the
visitor allows measurement. `cookieless_mode: 'never'` turns off PostHog's
cookieless capture. See [integrations](/docs/integrations/overview) for other
vendors.

Wrap your app with it in `src/main.tsx`:

```tsx title="src/main.tsx"
import { createRoot } from 'react-dom/client';

import { App } from './app';
import { Consent } from './consent';

const root = document.getElementById('root');
if (!root) {
	throw new Error('Missing #root element');
}
createRoot(root).render(
	<Consent>
		<App />
	</Consent>
);
```

Keep `Consent` mounted for the life of the app. If you use a client-side
router, render the router inside `Consent` so navigation does not remount the
provider. There is no stylesheet to import: the banner and dialog render their
own rules as `<style>` elements.

## Gate your own features

Read a category's permission with `useConsent` inside the provider:

```tsx title="src/marketing-banner.tsx"
import { useConsent } from 'c15t/react';

export function MarketingBanner() {
	return useConsent('marketing') ? <aside>Spring sale</aside> : null;
}
```

`useConsent` returns the permission right now. Under an opt-out policy it can be
`true` before the visitor has chosen anything. Read
[how consent works](/docs/concepts/how-consent-works#a-permission-is-not-a-recorded-choice)
before you record or report choices. For iframes, use
[`ConsentGate`](/docs/frameworks/react/components/consent-gate).

## Check that it works

Build and preview the production bundle with `vite build` and `vite preview`,
then open the site in a private window with DevTools open on the Network tab.
Test under a policy that asks for a choice, such as an EU opt-in policy.

1. **Before a choice.** The banner shows, with no `/manifest` request. There
   are no requests to `posthog.com`.
2. **Reject All.** The banner closes and the vendor requests stay absent.
3. **Reload.** The banner stays closed and the vendor requests stay absent.
4. **Privacy settings.** The footer link opens the dialog with **Analytics** (the
   `measurement` category) and **Marketing** off. Turn on **Analytics** and
   save. PostHog's `array.js` loads.
5. **Turn Analytics off again.** The page reloads and PostHog does not load.

If the banner never appears, check the generated policy and any location
request to `/init` on your backend URL. [Troubleshooting](/docs/frameworks/react/troubleshooting)
covers the common causes. [Verify consent](/docs/guides/verify-consent) has the
full release checklist.

## Next steps

* [Rendering](/docs/frameworks/react/rendering) for server-rendered React apps and
  what the first paint contains.
* [Scripts and embeds](/docs/frameworks/react/scripts) for more vendors, network
  blocking and data cleanup.
* [Customize](/docs/frameworks/react/customize) for colors, fonts, layout and copy.
* [Components](/docs/frameworks/react/components/consent-provider) for
  every provider option and component prop.
* The runnable app in `examples/react` of the
  [c15t repository](https://github.com/c15t/c15t) contains this setup.
