Skip to main content

How to add Google Tag Manager to Next.js with c15t

Add Google Tag Manager to a Next.js App Router project with c15t, then use Consent Mode v2 so GTM-managed tags wait for the right consent state.

Guides · · 8 min read

Google Tag Manager is easy to add to a page and hard to configure correctly. A Next.js app can load the GTM container in one place, but the tags inside that container still need consent signals before they fire analytics, ads, or conversion code.

c15t handles the browser-side consent state and loads the GTM container with Google Consent Mode v2 defaults set to denied. GTM can then re-evaluate its tags when the visitor accepts, rejects, or changes consent.

What you will build

By the end, you can:

  • Add c15t to a Next.js App Router project.
  • Register Google Tag Manager through @c15t/scripts/google-tag-manager.
  • Enable Consent Overview in GTM.
  • Create a consent-update trigger for tag re-evaluation.
  • Push application events to window.dataLayer without adding extra consent guards in React.
  • Verify the setup in GTM Preview and Google Tag Assistant.

This guide uses a hosted c15t backend and the prebuilt c15t consent UI. If your app already has c15t installed, start at the GTM registration step.

How c15t and GTM split the work

Most c15t script integrations wait until the matching consent category is granted before the vendor script enters the DOM. GTM is different.

The c15t GTM integration loads the container on page start because GTM has its own consent system. c15t sets Consent Mode v2 defaults to denied, then pushes updates when the visitor changes consent. In the c15t docs, the GTM integration is categorized as necessary, uses alwaysLoad, and sends a default consent-update event on consent change.

That means your app should not add the normal GTM snippet with next/script. It should not add a second GTM container in app/layout.tsx either. Let c15t own the GTM script so the default denied state and later updates happen in the expected order.

Google’s Consent Mode guide says a site needs a default consent state before tags send measurement data, then an update after the user interacts with consent controls. Google also notes that code order matters because late defaults may not work as intended.

Install c15t and the script helpers

Install the Next.js package and the built-in script integrations package.

npm install @c15t/nextjs @c15t/scripts

Import the c15t stylesheet from your app-level CSS file if you use the prebuilt banner and dialog.

/* app/globals.css */
@import "@c15t/nextjs/styles.css";

You will also need two public setup values:

  • Your GTM container ID, such as GTM-XXXXXXX.
  • Your hosted c15t backend URL, such as https://your-instance.c15t.dev.

These values are not secrets. The browser can see the GTM container ID and the c15t backend URL once the site loads, so this guide keeps them directly in the example provider.

Register Google Tag Manager in the c15t provider

Create a client provider and pass the GTM helper through the scripts option.

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

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

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

Replace GTM-XXXXXXX and https://your-instance.c15t.dev with your real values. If your staging and production sites use different GTM containers or c15t backends, choose the value in your app configuration. You do not need to treat either value as a secret.

Mount the provider at the app root.

// app/layout.tsx
import { ConsentProvider } from '@/components/consent-manager/provider';
import './globals.css';

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

Start the app and confirm the consent banner appears. Then open the browser Network panel and check for one GTM container request. If you see another GTM request from next/script, a CMS plugin, or a hardcoded snippet, remove the duplicate before you test consent behavior.

Add the trigger to your GTM tags

Open each measurement or marketing tag that should run after consent is granted. Add the consent-update trigger alongside the normal trigger for that tag.

Screenshot to add from the c15t docs before publication: Add the consent-update trigger to a GTM tag.

Then review Advanced Settings > Consent Settings for the tag. Google tags may have built-in consent checks. You can also require additional consent checks so the tag only fires when the required consent types are granted.

A common starting point is:

Article data table
Tag typeTriggerConsent review
GA4 page view or eventPage view plus consent-updateCheck analytics consent behavior, including analytics_storage.
Google Ads conversionConversion trigger plus consent-update when relevantReview ad-related consent types with your legal and ads teams.
Non-Google marketing pixelIts normal event trigger plus consent-update if it should fire after acceptanceAdd required consent checks if the tag does not have built-in checks.
Strictly necessary operational tagIts operational triggerMark as no additional consent required only after review.

The exact mapping depends on your site, regions, vendor contracts, and policy. Treat the table as an implementation prompt, not a legal classification.

Push application events to the data layer

Because the c15t GTM integration uses alwaysLoad, the GTM container and window.dataLayer are available from page start. c15t sets Consent Mode v2 defaults to denied before the visitor chooses, and GTM-managed tags decide whether they can fire.

That means you can push app events without wrapping every dataLayer.push() in useConsentManager().has(...).

// lib/gtm.ts
declare global {
  interface Window {
    dataLayer?: Array<Record<string, unknown>>;
  }
}

export function pushGtmEvent(event: string, payload: Record<string, unknown> = {}) {
  if (typeof window === 'undefined') {
    return;
  }

  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    event,
    ...payload,
  });
}

Use that helper from client components.

// components/signup-button.tsx
'use client';

import { pushGtmEvent } from '@/lib/gtm';

export function SignupButton() {
  return (
    <button
      type="button"
      onClick={() => {
        pushGtmEvent('signup', {
          plan: 'team',
          source: 'pricing-page',
        });
      }}
    >
      Start trial
    </button>
  );
}

This is different from integrations like Meta Pixel or some analytics SDKs, where the vendor global may not exist until consent is granted. For those integrations, guard calls with c15t consent state. We cover that pattern in How to integrate Meta Pixel in Next.js with c15t and How to add PostHog to Next.js with GDPR-aware consent.

Verify the setup

Use GTM Preview first because it shows the container event timeline and tag decisions.

  1. Open GTM Preview mode.
  2. Load your local or staging site.
  3. Confirm the GTM container loads on page load.
  4. Before consent, confirm non-essential tags do not fire.
  5. Accept consent in the c15t banner or dialog.
  6. Confirm a consent-update event appears in the GTM timeline.
  7. Confirm tags with matching consent checks fire after the update.
  8. Revoke consent, then confirm another update appears and affected tags stop firing.

Then use Google Tag Assistant to inspect Consent Mode. Google’s debugging guide says to check that the default consent state is set before tags fire, that updates happen after the visitor chooses, and that tags check the expected consent types.

For a Google-focused setup, check at least these Consent Mode v2 fields:

  • ad_storage
  • ad_user_data
  • ad_personalization
  • analytics_storage

If Tag Assistant reports that default consent was set too late, check for another GTM snippet or Google tag that runs outside c15t. Also check whether a tag in GTM is trying to set consent from a custom HTML tag. Google recommends Consent Initialization for consent-setting tags inside GTM and warns that some commands are not guaranteed to be available before the next trigger fires.

Common setup mistakes

The most common mistake is loading GTM twice. One copy may be managed by c15t and another may come from next/script, a marketing plugin, or a tag added in a layout file. Remove the unmanaged copy.

The second mistake is treating Consent Mode as a replacement for tag review. Consent Mode gives GTM consent state. It does not decide which of your tags are analytics, marketing, functional, or necessary.

The third mistake is pushing events only at page load. If a visitor grants consent after the page view, a tag that only listens to the initial page-view event may not fire. Add the consent-update trigger where you need tags to re-evaluate after consent changes.

The fourth mistake is testing only the accept path. Test reject, accept, customize, reload, and revoke. If your app has region-specific behavior, test those regions too.

Sources and further reading

  • c15t Google Tag Manager integration
  • c15t Next.js quickstart
  • c15t Script Loader
  • Google: Set up consent mode on websites
  • Google: Tag Manager consent mode support
  • Google: Troubleshoot consent mode with Tag Assistant
  • ICO: Cookies and similar technologies
  • EDPB: Guidelines 05/2020 on consent under Regulation 2016/679

Next steps

You now have a Next.js setup where c15t loads GTM, sends denied-by-default Consent Mode v2 state, and pushes updates when visitors change their consent choices.

You can now:

  • Keep GTM out of next/script and register it through c15t.
  • Configure Consent Overview and tag-level consent checks in GTM.
  • Add a consent-update trigger so tags re-check consent after the banner interaction.
  • Push app events to window.dataLayer without duplicating consent checks in every component.

Before publishing your GTM container, ask your legal or privacy reviewer to confirm the consent categories, vendor descriptions, regional defaults, and privacy notice updates.