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.
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/integrationsAdd your Inth project URL to .env.local. Copy it from your Inth dashboard.
.env.local
NEXT_PUBLIC_C15T_BACKEND_URL=https://your-project.inth.appWrap 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.
Google Consent Mode v2 in Next.js
The Google tag helper sends the Consent Mode v2 default before your config and an update after each choice.
import { gtag } from '@c15t/integrations/google-tag';
export const scripts = [gtag({ id: 'G-XXXXXXXXXX', category: 'measurement' })];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 and Google receives 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.
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:
- Under an opt in policy, no PostHog request leaves the page before a choice.
- After Reject All and a reload, PostHog stays off.
- Turning on measurement in Privacy settings loads it.
- 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
Yes. The root layout stays a Server Component, and resolveConsent() reads consent on the server.
Add each tag’s c15t helper to c15t.config.ts and remove its own script. Guides: GTM, Meta Pixel, LinkedIn and PostHog.
Use the c15t Google tag helper. It sends the default before your config and an update after each choice.
Set a policy for each region in your Inth project. resolveConsent() reads your host’s location headers, such as x-vercel-ip-country on Vercel, and applies the matching policy. Regional policies.
The banner ships in your bundle, so there is no vendor script to fetch first. Measure your own pages in Lighthouse and field data before and after you add it.
c15t has a local mode, but it keeps no consent records. Use an Inth project in production.
Click Reject All, reload and check that no request goes to the tag. Playwright recipe.
Yes. The c15t helpers keep both off until the visitor accepts measurement.