Implementing NextJS Cookie Consent with Google Tag Manager and Google Analytics GDPR-Compliant

Table of Contents

Intro

This guide will walk you through implementing a GDPR compliant Google Analytics setup using Google Tag Manager (GTM) in Next.js 15, TypeScript , complete with a proper cookie consent management system.

What is GDPR Compliance?

The General Data Protection Regulation (GDPR) requires websites to:
  • Get explicit consent before collecting user data
  • Allow users to opt out of non-essential tracking
  • Provide clear information about data collection
  • Enable users to change their preferences at any time

What is Google Tag Manager (GTM)?

GTM is a tag management system that allows you to:
  • Deploy various tracking scripts (tags)
  • Manage multiple analytics tools
  • Control when and how tags fire based on user consent
  • Update tracking implementations without changing code

What is Google Analytics 4 (GA4)?

GA4 is Google’s latest analytics platform that:
  • Offers enhanced privacy features
  • Supports consent mode
  • Provides flexible data collection options
  • Works seamlessly with GTM

Implementation Guide

Setup Dependencies

I’m using the vanilla-cookieconsent package. It comes with a predefined design.
First, add the required package to your Next.js project:
				
					yarn add vanilla-cookieconsent
				
			

Initialize GTM with Default Denied State

In your Next.js layout file, initialize GTM with all tracking disabled by default. Dont forget to set your GTM ID
				
					// app/layout.tsx
import Script from 'next/script';

export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        <Script id='gtm-script' strategy='afterInteractive'>
            {`
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('consent', 'default', {
              'ad_storage': 'denied',
              'analytics_storage': 'denied',
              'personalization_storage': 'denied',
              'functionality_storage': 'denied',
              'ad_user_data': 'denied',
              'ad_personalization': 'denied',
            });
            (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
            new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
            j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
            'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
            })(window,document,'script','dataLayer','YOUR GTM ID');
          `}
          </Script>
      </head>
      <body>{children}</body>
    </html>
  );
}
				
			

Implement a Cookie Consent Banner

Create a cookie consent component that:
  • Shows a privacy notice
  • Allows granular consent choices (Accept all, Only Necessary, Manual setting)
  • Updates GTM consent state based on user choices
  • Implement the consent update logic
  • Make sure to set your Domain or use a Environment Variable (Line 62)
				
					// components/CookieConsent.tsx
'use client';

import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import 'vanilla-cookieconsent/dist/cookieconsent.css';
import * as CookieConsent from 'vanilla-cookieconsent';

interface CookieConsentContextType {
  acceptedServices: string[];
}

const CookieConsentContext = createContext<CookieConsentContextType | undefined>(undefined);

export const useCookieConsent = (): CookieConsentContextType => {
  const context = useContext(CookieConsentContext);
  if (!context) {
    throw new Error('useCookieConsent must be used within a CookieConsentProvider');
  }
  return context;
};

interface CookieConsentProviderProps {
  children: ReactNode;
}

export const CookieConsentProvider: React.FC<CookieConsentProviderProps> = ({ children }) => {
  const [acceptedServices, setAcceptedServices] = useState<string[]>([]);

  useEffect(() => {
    const updateGtmConsent = () => {
      if (typeof window.gtag !== 'function') return;

      const userPreferences = CookieConsent.getUserPreferences();
      const acceptedCategories: string[] = userPreferences.acceptedCategories || [];
      const acceptedServices = userPreferences.acceptedServices || {};

      // Check if analytics category is accepted AND google is specifically enabled
      const isGoogleAccepted = acceptedCategories.includes('analytics') &&
        acceptedServices.analytics?.includes('google');

      const consentUpdate = {
        analytics_storage: isGoogleAccepted ? 'granted' : 'denied',
        ad_storage: isGoogleAccepted ? 'granted' : 'denied',
        personalization_storage: isGoogleAccepted ? 'granted' : 'denied',
        functionality_storage: 'granted',
        ad_user_data: isGoogleAccepted ? 'granted' : 'denied',
        ad_personalization: isGoogleAccepted ? 'granted' : 'denied',
      };

      window.gtag('consent', 'update', consentUpdate);
    };

    const updateAcceptedServices = () => {
      const userPreferences = CookieConsent.getUserPreferences();
      const acceptedServices = userPreferences.acceptedServices || [];
      const acceptedServicesList = Object.values(acceptedServices).flat();
      setAcceptedServices(acceptedServicesList);
    };

    CookieConsent.run({
      cookie: {
        domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN || 'your-domain.com',
      },
      guiOptions: {
        consentModal: { layout: 'cloud' },
      },
      categories: {
        necessary: {
          enabled: true,
          readOnly: true,
        },
        analytics: {
          services: {
            google: { label: 'Google Analytics' },
          },
        },
      },
      language: {
        default: 'en',
        translations: {
          en: {
            consentModal: {
              title: 'Privacy Settings',
              description: 'We use cookies to enhance your experience. Please choose your preferences.',
              acceptAllBtn: 'Accept all',
              acceptNecessaryBtn: 'Accept only necessary cookies',
              showPreferencesBtn: 'Manage preferences',
            },
            preferencesModal: {
              title: 'Cookie Preferences',
              sections: [
                {
                  title: 'Necessary',
                  description: 'Required for the site to function.',
                  linkedCategory: 'necessary',
                },
                {
                  title: 'Analytics',
                  description: 'Helps us understand site usage.',
                  linkedCategory: 'analytics',
                },
              ],
              acceptAllBtn: 'Accept all',
              acceptNecessaryBtn: 'Accept only necessary cookies',
              savePreferencesBtn: 'Save preferences',
            },
          },
        },
      },
      onChange: () => {
        updateAcceptedServices();
        updateGtmConsent();
      },
      onFirstConsent: () => {
        updateAcceptedServices();
        updateGtmConsent();
      },
    });

    const existingPreferences = CookieConsent.getUserPreferences();
    if (existingPreferences && existingPreferences.acceptedCategories) {
      updateAcceptedServices();
      updateGtmConsent();
    }
  }, []);

  return <CookieConsentContext.Provider value={{ acceptedServices }}>{children}</CookieConsentContext.Provider>;
};
				
			

Use CookieConsentProvider

Use the created Provider
				
					// app/providers.tsx
'use client';

import { CookieConsentProvider } from '@/components/CookieConsent';

interface ProvidersProps {
  children: React.ReactNode;
}

export default function Providers({ children }: ProvidersProps) {
  return (
    <CookieConsentProvider>
      {children}
    </CookieConsentProvider>
  );
}
				
			

Use Provider in layout

Use the Provider code in our Layout file

				
					// app/layout.tsx
import Providers from '@/app/providers';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <head>
        <Script id="gtm-script" strategy="afterInteractive">
          {`
            // Your GTM initialization code here
          `}
        </Script>
      </head>
      <body>
        <Providers>
          {children}
        </Providers>
      </body>
    </html>
  );
}
				
			

Adding New Tracking Services to Cookie Consent

  1. Add a new Service to e.g. the Analytics Categories inside CookieConsent.run()
  2. Add new function e.g. updateXConsent()
    1. Add usage in onChange and onFirstConsent
  3. Update the Modal Text if you added a new category

Adding a Privacy Settings Page to Your Next.js App

One crucial aspect of cookie consent management is providing users with easy access to their privacy settings after their initial choice. Here’s how to implement a dedicated privacy settings page:
				
					// app/settings/privacy/page.tsx
'use client';

import { Button, Stack, Text } from '@mantine/core'; // or any UI library
import * as CookieConsent from 'vanilla-cookieconsent';

export default function PrivacyPage() {
  return (
    <div className="p-4">
      <h1 className="text-2xl font-bold mb-6">Privacy Settings</h1>

      {/* Cookie Settings */}
      <div className="mb-8 p-4 border rounded-lg">
        <h2 className="text-xl font-semibold mb-4">Cookie Settings</h2>
        
        <Stack gap="md">
          <Text>
            Control how we use cookies on our website. Click the button below
            to manage your cookie preferences.
          </Text>

          <Button
            onClick={() => CookieConsent.showPreferences()}
            className="w-fit"
          >
            Manage Cookies
          </Button>
        </Stack>
      </div>
    </div>
  );
}
				
			

Conclusion

Implementing GDPR-compliant analytics requires careful attention to:
  • User privacy rights
  • Technical implementation
  • Consent management
  • Data handling practices
By following this guide, you’ve created a robust, compliant analytics setup that respects user privacy while maintaining valuable insights into your application’s usage.

Links

 

Best, Julian

Portrait Julian Geißler

About me

Serving clients worldwide

Based in the technology region between Frankfurt, Darmstadt, Mannheim, and Worms, I provide professional web development and software development primarily remotely for clients worldwide. When needed, I’m also available for important meetings in person in the Rhine-Main region.

Julian Geißler - Programmer

With over 5 years of experience and a Master’s degree in Computer Science, I offer technical expertise and direct communication. My focus is on scalable solutions using Next.js, React, Laravel, and Django, complemented by AI integration and technical SEO. From conception through development to hosting, I guide your project with tailored solutions and a clear understanding of your business requirements

My region in Hesse: Frankfurt, Darmstadt, Mannheim, and Worms

Contact

Talk to me
As a software developer, I help you bring your digital ideas to life

My Socials