Skip to content

Add a cookie consent banner to Next.js

c15t puts the banner in your Next.js app as code. It works with the App Router, holds tags until the visitor chooses and keeps Google Consent Mode v2 in sync.

Ask your coding agent
Sample Consent banner with c15t’s English strings

Install c15t in a Next.js 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.

npm install c15t @c15t/integrations

Add your Inth project URL to .env.local. Copy it from your Inth dashboard.

.env.local

NEXT_PUBLIC_C15T_BACKEND_URL=https://your-project.inth.app

Wrap your Next.js config. The build fetches your consent policy once and caches it, so rebuild after you change a policy.

next.config.ts

import { withConsentManifest } from 'c15t/next/build';
import type { NextConfig } from 'next';

const nextConfig = {} satisfies NextConfig;

export default withConsentManifest(nextConfig);

Prefer one command? npx @inth/cli c15t connects your Inth project and starts your agent with the c15t setup steps for you to review. Code on this page comes from the c15t Next.js quickstart, checked 28 October 2026.

App Router and Pages Router setup

In the App Router, the root layout stays a Server Component, so you write no 'use client' wrapper. Pass resolveConsent() to ConsentRoot without awaiting it. It reads the visitor’s cookies and location headers on the server, so the browser makes no /init request.

app/layout.tsx

import { ConsentBanner, ConsentDialog, ConsentDialogLink, ConsentRoot } from 'c15t/next';
import { resolveConsent } from 'c15t/next/server';
import type { ReactNode } from 'react';
import './globals.css';

const RootLayout = ({ children }: { children: ReactNode }) => (
  <html lang="en">
    <body>
      {/* Not awaited: the page renders while consent resolves. */}
      <ConsentRoot state={resolveConsent()}>
        {children}
        <ConsentBanner />
        <ConsentDialog />
        <footer>
          <ConsentDialogLink>Privacy settings</ConsentDialogLink>
        </footer>
      </ConsentRoot>
    </body>
  </html>
);

export default RootLayout;

The Pages Router, static export with output: 'export' and client only setups each have their own guide in the c15t Next.js docs.

Hold scripts until consent

List each tag in c15t.config.ts. With loadMode: 'after-consent', the script stays off the page until the visitor accepts its category.

c15t.config.ts

import { posthog } from '@c15t/integrations/posthog';
import { defineConsentConfig } from 'c15t/next';

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

Remove the tag’s own script or SDK setup so it loads only through c15t. Step by step guides for common tags:

Vercel Analytics and Speed Insights

If you deploy on Vercel, hold its analytics until the visitor accepts measurement. Remove the Analytics and SpeedInsights components first, then add the c15t helpers.

import { vercelAnalytics } from '@c15t/integrations/vercel-analytics';
import { vercelSpeedInsights } from '@c15t/integrations/vercel-speed-insights';

// Add these to the scripts array in c15t.config.ts
scripts: [vercelAnalytics(), vercelSpeedInsights()],

Until measurement has consent, Vercel Analytics stays off the page and Speed Insights drops every Web Vitals report. Neither helper sends a consent signal to Vercel. See the Vercel Analytics and Speed Insights guides.

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

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

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, resolveConsent() reads the visitor’s location from your host’s headers, such as x-vercel-ip-country on Vercel, then applies the matching policy.

If your host strips those headers before Server Components run, add c15tProxy in proxy.ts on Next.js 16 or c15tMiddleware in middleware.ts on Next.js 15. Without location headers, c15t applies your unknown location rule. See geography headers.

Test each region before launch. See how regional policies work.

Keep the banner off your critical path

The banner ships in your bundle, so there is no vendor script to fetch first. Next.js renders the page without waiting for consent. The banner renders on the server and arrives later in the same response, before the page hydrates. Pass streamBanner: false to mount it after hydration instead.

In CookieBench’s Next.js desktop run, the c15t banner appeared in a median 27 ms. CookieBench cookiebench-v3.2.4, published 12 October 2026. Inth, the team behind c15t, maintains CookieBench, and c15t is one of the tools measured. Lab results; your site may differ. Run limits.

Measure your own pages too. A lab result is not a field result.

Test consent gates with Playwright

Check these four things before you ship:

  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. Turning on measurement in Privacy settings loads it.
  4. Turning measurement off reloads the page without it.

Choices post to your Inth project, so you can check that each one was recorded. Write these as Playwright tests.

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.

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.

Questions about Next.js consent