Skip to main content

React

Quickstart

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. 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 for what that means for the first paint.

You need an Inth 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 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

npm install 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:

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:

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

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

CommandDefault when the fetch fails
Production build: next build, vite build, nuxt build, astro buildThe build stops with an error.
Dev: next dev, vite dev, nuxt dev, astro devA 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:

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

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 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 for other vendors.

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

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:

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 before you record or report choices. For iframes, use ConsentGate.

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 covers the common causes. Verify consent has the full release checklist.

Next steps

  • Rendering for server-rendered React apps and what the first paint contains.
  • Scripts and embeds for more vendors, network blocking and data cleanup.
  • Customize for colors, fonts, layout and copy.
  • Components for every provider option and component prop.
  • The runnable app in examples/react of the c15t repository contains this setup.