[Consent banner](/consent-banner)

# Add a cookie consent banner to TanStack Start

c15t puts the banner in your TanStack Start app as code. It resolves consent on the server, 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**: TanStack Start 1.168.60 and later 1.x, React 18 or 19
- **Library**: c15t, open source
- **Code checked**: 28 October 2026

## Install c15t in a TanStack Start 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 @c15t/integrations
```

Add your Inth project URL to `.env`. Copy it from your Inth dashboard. Vite writes it into the server and browser builds, so set it before you build.

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

Add the `consentManifest` plugin before `tanstackStart()`. During the build, it downloads your consent policy and keeps it on the server. Rebuild after you change a policy.

```ts
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
import viteReact from '@vitejs/plugin-react';
import { consentManifest } from 'c15t/tanstack-start/build';
import { defineConfig } from 'vite';

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

Code on this page comes from the [c15t TanStack Start quickstart](https://c15t.com/docs/frameworks/tanstack-start/quickstart), checked 28 October 2026.

This setup needs the Start server. For SPA mode, prerendered pages or a static host, see [TanStack Start rendering](https://c15t.com/docs/frameworks/tanstack-start/rendering).

## Resolve consent in the root route

A server function in the root loader reads the consent cookie and the location headers of the visitor. It resolves the policy from the bundled snapshot, so the render does not wait for Inth.

```tsx
import { posthog } from '@c15t/integrations/posthog';
import {
  createRootRoute,
  HeadContent,
  Outlet,
  Scripts,
} from '@tanstack/react-router';
import { createServerFn } from '@tanstack/react-start';
import {
  ConsentBanner,
  ConsentDialog,
  ConsentDialogLink,
  ConsentRoot,
} from 'c15t/tanstack-start';
import {
  consentLoaderOptions,
  createConsentStateHandler,
} from 'c15t/tanstack-start/server';

// Start's compiler keeps the handler and the bundled policy on the server.
const getConsentState = createServerFn({ method: 'GET' }).handler(
  createConsentStateHandler()
);

const scripts = [
  posthog({
    id: 'phc_your_project_key',
    initOptions: { cookieless_mode: 'never' },
    loadMode: 'after-consent',
  }),
];

const RootComponent = () => {
  const { consent } = Route.useLoaderData();
  return (
    <html lang="en">
      <head>
        <HeadContent />
      </head>
      <body>
        <ConsentRoot state={consent} scripts={scripts}>
          <Outlet />
          <ConsentBanner />
          <ConsentDialog />
          <footer>
            <ConsentDialogLink>Privacy settings</ConsentDialogLink>
          </footer>
        </ConsentRoot>
        <Scripts />
      </body>
    </html>
  );
};

export const Route = createRootRoute({
  ...consentLoaderOptions,
  component: RootComponent,
  head: () => ({
    meta: [
      { charSet: 'utf-8' },
      { content: 'width=device-width, initial-scale=1', name: 'viewport' },
    ],
  }),
  loader: async () => ({ consent: await getConsentState() }),
});
```

Declare the `createServerFn()` call in your own module, as shown. The Start compiler finds it there and keeps the policy out of the browser build. `consentLoaderOptions` makes sure that the loader does not run again on client navigation.

## Hold scripts until consent

Keep the `scripts` array in the root route module. A server function returns only data, and vendor helpers contain functions. With `loadMode: 'after-consent'`, PostHog stays off the page until the visitor allows measurement.

Remove every other loader for PostHog, so it loads once and only through c15t. [TanStack Start scripts and embeds](https://c15t.com/docs/frameworks/tanstack-start/scripts) covers more vendors, iframes and the network blocker. The [c15t integrations](https://c15t.com/docs/integrations/overview) list the helper for each vendor.

## Gate your own features with useConsent

Read the permission for a category with `useConsent` from `c15t/tanstack-start`. Show a feature only after the visitor gives consent to its category.

```tsx
import { useConsent } from 'c15t/tanstack-start';

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

Under an opt out policy, the permission can be `true` before the visitor chooses.

## Google Consent Mode v2 in TanStack Start

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 in `src/routes/__root.tsx`, 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. On the server, `createConsentStateHandler()` reads the location header that your host adds, such as `x-vercel-ip-country`, and applies the policy for that location. [TanStack Start geography headers](https://c15t.com/docs/frameworks/tanstack-start/geography-headers) lists the headers it reads.

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

## Keep the banner in the first HTML

The server resolves consent before it renders, so the banner is part of the first HTML. Most scripts that have the visitor’s consent start before the page hydrates. A visitor who already chose gets HTML with no banner.

Measure your own pages before and after you add the banner.

## Check that consent gates work

Run a production build, start the server and open a private window with the Network tab open. On localhost, your host adds no location header. For the test only, pass `country: 'DE'` to `createConsentStateHandler()` to get the EU policy.

1. The first HTML contains the banner.
2. No PostHog request leaves the page before a choice.
3. After Reject All and a reload, PostHog stays off.
4. When you turn on measurement in Privacy settings, PostHog loads.

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), [Nuxt](/consent-banner/nuxt), [Vue](/consent-banner/vue), [Astro](/consent-banner/astro), [Svelte](/consent-banner/svelte), [SvelteKit](/consent-banner/sveltekit), [HTML](/consent-banner/html) and [JavaScript](/consent-banner/javascript).

Building a React app without a server? See the [React cookie consent banner](/consent-banner/react).

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

## Questions about TanStack Start consent

### Is the banner in the server HTML?

Yes. A server function in the root loader resolves consent before the render, so the first HTML contains the banner.

### Which TanStack Start versions does it support?

`@tanstack/react-start` 1.168.60 or later in 1.x, including 1.0, on React 18 or 19.

### Does it work in SPA mode or on a static host?

Yes, with a different setup. [TanStack Start rendering](https://c15t.com/docs/frameworks/tanstack-start/rendering) covers SPA mode, prerendered pages and static hosts.

### Why do the scripts stay out of the server function?

A server function returns only data. Vendor helpers contain functions, so keep the `scripts` array in the root route module.

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