Skip to content

Blog

Guides

How to implement Consent Mode v2 as a product control

Implement Google Consent Mode v2 with denied defaults, clear consent mapping, update calls, and consent records your team can review later.

Google Consent Mode v2 is required for EU traffic. Here's what developers actually need to know.

If you've received an email from Google about "Consent Mode v2", you're not alone. It is one of those things that gets explained with far more jargon than it needs.

Here is the simple version: Consent Mode v2 is just a way to tell Google what the user has actually consented to, before Google starts doing anything with that data.

If you are using Google Ads, GA4, or Google Tag Manager for visitors in the EEA, you need to get this right. Not eventually. Up front.

Why Developers Should Care

Without a proper Consent Mode v2 setup, Google measurement and advertising get messy fast. You can end up with:

  • broken or partial conversion tracking
  • unreliable attribution
  • remarketing issues
  • tags firing in the wrong state
  • consent signals arriving too late to be useful

This is not really a marketing problem. It is an ordering problem.

The important part is not just sending consent updates. It is making sure the default consent state exists early enough that Google tools do not make assumptions before the user has made a choice.

The Part Most Guides Skip

Most Consent Mode explanations make it sound like you need to hand-roll gtag('consent', ...) calls everywhere.

You usually do not.

With current c15t Next.js setup, c15t from Inth gives you a ConsentManagerProvider at the app root, handles the initialization flow, reads existing consent, resolves policy and location, and decides whether the banner should show before the first meaningful consent-aware render.

That is the real job.

The Current Next.js Setup

If you are starting fresh, the current Next.js quickstart looks more like this:

// components/consent-manager/provider.tsx
'use client';

import { type ReactNode } from 'react';
import {
  ConsentManagerProvider,
  ConsentBanner,
  ConsentDialog,
} from '@c15t/nextjs';

export default function ConsentManagerClient({ children }: { children: ReactNode }) {
  return (
    <ConsentManagerProvider
      options={{
        mode: 'hosted',
        backendURL: 'https://your-instance.c15t.dev',
        consentCategories: ['necessary', 'measurement', 'marketing'],
      }}
    >
      <ConsentBanner />
      <ConsentDialog />
      {children}
    </ConsentManagerProvider>
  );
}

Then mount it at the app root:

// app/layout.tsx
import { ConsentManager } from '@/components/consent-manager';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <ConsentManager>{children}</ConsentManager>
      </body>
    </html>
  );
}

That gets the consent system in place. Then you add Google integration based on how your stack is set up.

If You Use gtag.js Directly

If you are wiring GA4 or Google Ads directly in code, use the GA4 + Google Ads integration:

import { type ReactNode } from 'react';
import { ConsentManagerProvider } from '@c15t/react';
import { gtag } from '@c15t/scripts/google-tag';

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

export function ConsentProvider({ children }: { children: ReactNode }) {
  return (
    <ConsentManagerProvider
      options={{
        mode: 'hosted',
        backendURL: 'https://your-instance.c15t.dev',
        scripts,
      }}
    >
      {children}
    </ConsentManagerProvider>
  );
}

The important detail is this: file gtag.js can load from page start because c15t sets Consent Mode defaults to denied first. When consent changes, c15t pushes the update automatically.

So yes, window.gtag?.('event', 'sign_up') is safe to call, because the Google SDK suppresses transmission while the relevant consent is denied.

If You Use Google Tag Manager

If your team manages tags in GTM, use the Google Tag Manager integration:

import { type ReactNode } from 'react';
import { ConsentManagerProvider } from '@c15t/react';
import { googleTagManager } from '@c15t/scripts/google-tag-manager';

const scripts = [googleTagManager({ id: 'GTM-XXXXXXX' })];

export function ConsentProvider({ children }: { children: ReactNode }) {
  return (
    <ConsentManagerProvider
      options={{
        mode: 'hosted',
        backendURL: 'https://your-instance.c15t.dev',
        scripts,
      }}
    >
      {children}
    </ConsentManagerProvider>
  );
}

This is one place where developers often get confused.

GTM itself can load from the start. That does not mean your marketing tags are free to fire. c15t sets denied defaults first, GTM maintains its internal consent state, and GTM-managed tags only run when the matching consent has been granted.

You will usually also want to create a custom consent-update trigger inside GTM so existing tags re-evaluate when preferences change.

Common Mistakes to Avoid

1. Mixing old and new c15t examples

A lot of older examples use older package names or config shapes. If you are following the current docs, start with @c15t/nextjs for app setup and the current integrations overview.

2. Loading both GTM and gtag.js for the same destination

The docs are clear on this: use GTM if you want central tag management, or file gtag.js if you only need GA4 or Google Ads directly. Running both for the same destination can duplicate events.

3. Treating analytics and marketing as the same thing

c15t has explicit consent categories. Use measurement for analytics and marketing for advertising or conversion tracking. Do not collapse everything into one bucket because it feels quicker.

4. Ignoring policy resolution and re-prompting

Consent is not just "show banner once". The current policy pack model supports jurisdiction-aware policy resolution and re-prompting when materially relevant policy decisions change.

5. Forgetting the backend tradeoff

You can run c15t in offline mode, but the client modes docs are pretty direct about the tradeoff: you give up backend audit history, server-side awareness, and automatic jurisdiction detection. For most real production setups, hosted mode is the better default.

How to Verify It Is Working

A practical test is better than staring at config for an hour.

For GTM

Use GTM Preview and check:

  1. the container loads on page load
  2. non-essential tags do not fire before consent
  3. a consent-update event appears after the user makes a choice
  4. measurement or marketing tags only fire after the matching consent is granted
  5. revoking consent produces another update and stops affected tags

For gtag.js

With the Google Tag integration, check that Google is initialized with denied defaults first, then updated after consent is granted. If events are being called before consent, that is fine. They should still be suppressed until the relevant category is allowed.

For the overall consent lifecycle

Use DevTools while building and debugging, and review the Initialization Flow if banner timing or state changes seem odd.

The Bottom Line

Consent Mode v2 is not hard because the model is complicated.

It is hard because timing, state, and script behavior all need to line up.

c15t integrates consent controls with your application. In CookieBench’s current Next.js desktop test, its banner appeared in a median 27 ms, with +8 ms main-content delay and +104 KB initial transfer. On emulated mobile, the banner appeared in 240 ms and added 400 ms to main-content paint. Updated 12 October 2026: CookieBench results, Next.js desktop profile, analysis matrix-2026-10-12, methodology cookiebench-v3.2.4. CookieBench is maintained by Inth. The run used a local c15t backend and live services for other CMPs. Mobile results use emulation. These are lab results, not a guarantee for your site. See run limits.

c15t handles the awkward parts:

  • initializing consent state early
  • setting denied defaults for Google integrations
  • updating consent automatically when users change preferences
  • gating scripts and related behavior by category
  • supporting hosted, jurisdiction-aware consent flows in production

So the developer job is much simpler than it used to be: mount the provider correctly, choose the right Google integration, map your categories sensibly, and verify the behavior in preview tools.

That is it.

Want to hand this to your agent to deploy? Sign up to Inth, copy the prompt on sign up, and let your agent wire c15t into your repo against the matching docs. Then review the diff and ship.