by Shon Danvudi.
4/1/2025
Configuring Google Cookies Consent with Next.js 15.

This tutorial will guide you through implementing Google cookies consent in Next.js 15 (app router) without any additional packages.
1. Introduction & Test runs.
2. How to implement.
Introduction & Test runs.
The Third-Party Struggles
For anyone who has had to manage cookies and cookie consent in Next.js, you know the struggle. While integrating third-party solutions like@next/third-party/google
sounds appealing, the lack of flexibility and sparse documentation often leaves developers grappling with unanswered questions. Test runs showed that
@next/third-party/google
does not support dynamic consent set or update without using additional features of Google Tag Manager. Existing solutions either didn’t account for nuanced consent updates or failed to integrate seamlessly. For example, one of the most popular approaches you will see researching this problem is a simple (but not working) if—else statement & @next/third-party/google
initialization. Some users also suggest sending the “Consent” event using the same package, but that doesn’t even make sense, because to send an event, the tag has to be initialized, which already defeats the purpose of consent management. if(consent) <GoogleTagManager gtmId=”GTM-XYZ” /> //DOES NOT WORK!
// OR
sendGAEvent('Consent', 'update', {...consentPreference})} //DOES NOT WORK!
There are many other misleading or outdated guides on how to set cookie consent using
@next/third-party/google
, but it just DOES NOT WORK, or to be fair, is just not supported yet as of March 29, 2025. Keep in mind that Next.js 15 was released on October 21st, 2024 (the package was released a little earlier). But the framework being young doesn't cancel the problem of cookie consent. It was clear that I needed a customized approach.The Breakthrough Moment
After hours of experimentation and troubleshooting, I devised a custom solution to handle cookie consent effectively in Next.js while ensuring compatibility with GTM. The solution DOES NOT require any additional packages and dynamically sets and updates consent usinglocalStorage
, useEffect
and next/script
. The implementation worked flawlessly when tested with Google Tag Assistant.Let’s break it down. (This is how I was able to analyze and fix the issue. If you want the full code, scroll to How to implement.)
Google Analytics shows us this code snippet to install the tag. Obviously, just pasting the script into our project in the
layout.tsx
will cause issues because Next.js handles scripts and rendering in a way that is optimized for server-side rendering (SSR) and will not properly handle the loading of the Google Analytics script on the client side. <!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XYZ">
</script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XYZ');
</script>
So basically, we need to translate this to
next/script
. <Script strategy="afterInteractive"
src="https://www.googletagmanager.com/gtag/js?id=G-XYZ"/>
<Script
id="google-analytics"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html:'
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XYZ', {
page_path: window.location.pathname,
});
',
}}
/>
Pretty neat, but this doesn’t solve the problem with consent management. To implement cookie consent default & update, all we have to do is add the code provided by Google Developers Docs to the translated script.
gtag('consent', 'default', {
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'analytics_storage': 'denied'
});
And in Next.js it will be
<Script
id="google-analytics"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: '
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('consent', 'default', {
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'analytics_storage': 'denied'
});
gtag('config', 'G-XYZ', {
page_path: window.location.pathname,
});
',
}}
/>
Congratulations, you just set the default consent settings to denied.
This finally solved my struggle of dynamically managing cookie consent. Tag Assistant helped me to perfect this by letting me know that consent was being initialized and also updated before the default consent is set. So, make sure to add this component to layout.tsx above the body element.
To see how to update consent without Tag Assistant errors, keep reading.
How to implement.
Setting Default Consent
The key is to initialize the consent state after the client loads, avoiding potential errors caused by server-side rendering.'use client';
import Script from 'next/script';
import { useEffect, useState } from 'react';
export default function GoogleTag({ GA_ID }: { GA_ID: string }) {
const [consent, setConsent] = useState<string | null>(null);
// Set the consent value from localStorage after the client loads
// Assuming the answer is stored in localStorage called cookie
useEffect(() => {
setConsent(localStorage.getItem('cookie') === "true" ? 'granted' : 'denied');
}, [GA_ID]);
if (consent === null) return null;
return (
<>
<Script
strategy="afterInteractive"
src={'https://www.googletagmanager.com/gtag/js?id=$[GA_ID]'}
/>
<Script
id="google-tag-manager"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: '
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('consent', 'default', {
'ad_storage': '$[consent]',
'ad_user_data': '$[consent]',
'ad_personalization': '$[consent]',
'analytics_storage': '$[consent]'
});
gtag('config', '$[GA_ID]', {
page_path: window.location.pathname,
});
',
}}
/>
</>
);
}
/* As stated in the introduction,
this component should be called in
layout.tsx between <html> and <body> elements*/
Updating Consent Dynamically
This logic ensures changes are reflected promptly in GTM without re-renders and after the default is set.useEffect(() => {
// Skip the first render to avoid unnecessary updates
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
window.gtag("consent", "update", {
ad_storage: local ? "granted" : "denied",
ad_user_data: local ? "granted" : "denied",
ad_personalization: local ? "granted" : "denied",
analytics_storage: local ? "granted" : "denied",
});
}, [consent]); //consent being the localStorage item
Lessons learned and final thoughts
The path to a robust cookie consent solution was far from linear. It involved countless iterations and debugging sessions. However, the result — a streamlined, dynamic, and scalable solution — made the effort worthwhile.Sometimes, the most effective solutions aren’t readily available off the shelf but require innovation and persistence. If you find yourself struggling with third-party libraries for cookie consent, remember the flexibility of custom logic combined with a solid understanding of Next.js can empower you to build exactly what you need