TLDR: Cookie consent in Next.js should take minutes, not days. One Script tag in your layout, full GDPR compliance, zero GTM configuration. Works with App Router and Pages Router.
Read full summary
Next.js developers waste days configuring cookie consent. Google Consent Mode v2 docs, GTM triggers, timing issues. This guide shows the 2-minute path: add GetCookies, ship your feature, move on.
Summary by Claude AI
The Day We Almost Missed a Launch
We were shipping a client's Next.js app. Three days from launch.
"Hey, we need GDPR compliance for EU users."
We'd done this before. Or so we thought. We spent the next two days:
- Reading Google Consent Mode v2 documentation
- Configuring 23 GTM triggers
- Debugging why consent defaults weren't firing before gtag
- Watching my Lighthouse score drop from 98 to 74
- Still not being sure if it actually worked
We delayed the launch.
There's a better way. We wish we'd known it then.
The 2-Minute Setup
App Router (Next.js 13+)
// app/layout.tsx
import Script from 'next/script'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<head>
<Script
src="https://getcookies.co/api/v1/widget/loader.js"
data-domain-id="YOUR_DOMAIN_ID"
strategy="afterInteractive"
/>
</head>
<body>{children}</body>
</html>
)
}
Pages Router (Next.js 12 and earlier)
// pages/_app.tsx
import Script from 'next/script'
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Script
src="https://getcookies.co/api/v1/widget/loader.js"
data-domain-id="YOUR_DOMAIN_ID"
strategy="afterInteractive"
/>
<Component {...pageProps} />
</>
)
}
That's it. Refresh your site. You'll see a cookie banner.
You now have:
- GDPR-compliant consent collection
- Google Consent Mode v2 (all four signals)
- Automatic script blocking
- Third-party pixel consent (Meta, TikTok, Google)
- Consent logging for audits
No GTM. No configuration. No documentation rabbit holes.
Why afterInteractive?
Next.js Script component has three loading strategies:
| Strategy | When It Runs | Best For |
|---|---|---|
beforeInteractive |
Before page hydration | Critical, render-blocking scripts |
afterInteractive |
After hydration starts | GetCookies (recommended) |
lazyOnload |
During idle time | Non-essential scripts |
afterInteractive is the sweet spot because:
- Your page renders fast (good Lighthouse scores)
- GetCookies sets consent defaults before users interact
- The banner appears quickly after the page loads
Environment Variables: Dev vs Production
You probably want different domain IDs for development and production:
// app/layout.tsx
import Script from 'next/script'
const DOMAIN_ID = process.env.NEXT_PUBLIC_GETCOOKIES_DOMAIN_ID
export default function RootLayout({ children }) {
return (
<html>
<head>
{DOMAIN_ID && (
<Script
src="https://getcookies.co/api/v1/widget/loader.js"
data-domain-id={DOMAIN_ID}
strategy="afterInteractive"
/>
)}
</head>
<body>{children}</body>
</html>
)
}
Set up your environments:
# .env.local (development)
NEXT_PUBLIC_GETCOOKIES_DOMAIN_ID=dev-domain-id
# .env.production
NEXT_PUBLIC_GETCOOKIES_DOMAIN_ID=prod-domain-id
Create separate domains in the GetCookies dashboard for localhost, staging, and production. Each gets independent consent tracking.
Loading Analytics After Consent
Method 1: The JavaScript API
Want fine-grained control? Use the GetCookies events:
// components/Analytics.tsx
'use client'
import { useEffect } from 'react'
export function Analytics() {
useEffect(() => {
const handleConsent = (e: CustomEvent) => {
const { analytics, marketing } = e.detail
if (analytics) {
window.gtag?.('config', 'G-XXXXXXXX')
}
if (marketing) {
window.fbq?.('init', 'XXXXXXXXXX')
}
}
window.addEventListener('getcookies:consent', handleConsent as EventListener)
// Check if consent already exists
if (window.GetCookies?.hasConsent('analytics')) {
window.gtag?.('config', 'G-XXXXXXXX')
}
return () => {
window.removeEventListener('getcookies:consent', handleConsent as EventListener)
}
}, [])
return null
}
Method 2: Data Attributes (Simpler)
GetCookies can auto-manage scripts using data attributes:
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<head>
<Script
src="https://getcookies.co/api/v1/widget/loader.js"
data-domain-id="YOUR_DOMAIN_ID"
strategy="afterInteractive"
/>
{/* This script only loads after analytics consent */}
<script
data-cookieconsent="analytics"
type="text/plain"
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXX"
/>
</head>
<body>{children}</body>
</html>
)
}
The type="text/plain" prevents the script from running. GetCookies changes it to text/javascript after consent.
Server Components: No Special Handling Needed
React Server Components work seamlessly with GetCookies:
// app/page.tsx (Server Component)
export default function Page() {
return (
<main>
<h1>Welcome</h1>
{/* GetCookies banner renders client-side */}
{/* No hydration mismatch issues */}
</main>
)
}
GetCookies injects its UI into the DOM client-side. It doesn't require server rendering. Server Components render normally.
TypeScript Types
Add type definitions for autocomplete:
// types/getcookies.d.ts
declare global {
interface Window {
GetCookies?: {
hasConsent: (category: 'analytics' | 'marketing' | 'preferences') => boolean
showPreferences: () => void
acceptAll: () => void
rejectAll: () => void
}
}
}
export {}
Now you can use the API with full type safety:
'use client'
export function CookieSettingsButton() {
return (
<button onClick={() => window.GetCookies?.showPreferences()}>
Cookie Settings
</button>
)
}
Common Patterns
Footer Cookie Settings Link
// components/Footer.tsx
'use client'
export function Footer() {
return (
<footer className="py-8 border-t">
<button
onClick={() => window.GetCookies?.showPreferences()}
className="text-sm text-gray-500 hover:underline"
>
Cookie Settings
</button>
</footer>
)
}
Consent-Gated Personalization
'use client'
import { useState, useEffect } from 'react'
export function PersonalizedContent() {
const [hasConsent, setHasConsent] = useState(false)
useEffect(() => {
setHasConsent(window.GetCookies?.hasConsent('marketing') ?? false)
const handler = () => {
setHasConsent(window.GetCookies?.hasConsent('marketing') ?? false)
}
window.addEventListener('getcookies:consent', handler)
return () => window.removeEventListener('getcookies:consent', handler)
}, [])
if (!hasConsent) {
return <GenericRecommendations />
}
return <PersonalizedRecommendations />
}
Deploying Everywhere
GetCookies works on every Next.js deployment target:
Vercel: Works out of the box.
vercel --prod
Netlify: Works out of the box.
# netlify.toml
[build]
command = "npm run build"
publish = ".next"
Cloudflare Pages: Works with @cloudflare/next-on-pages.
Docker: Works in any container.
Troubleshooting
Banner not showing?
- Check your domain ID in the GetCookies dashboard
- Verify the domain is added and matches your URL
- Check browser console for errors
- Check Network tab to see if the script loads
Consent not persisting?
- Check that localStorage isn't being cleared
- Verify domain matches exactly (www vs non-www)
- Look for conflicting consent scripts
TypeScript errors? Add the types file above, or use the quick fix:
(window as any).GetCookies?.showPreferences()
Performance: The Numbers
GetCookies is built for speed:
| Metric | GetCookies | Typical CMP |
|---|---|---|
| Initial load | ~1.6 KB | 40-150KB |
| LCP impact | None | Often 100ms+ |
| CLS impact | None | Often significant |
| Lighthouse hit | 0-2 points | 10-20 points |
Your Lighthouse scores stay where they belong.
What You Don't Have to Do
- Configure GTM triggers
- Read Consent Mode v2 documentation
- Debug timing issues
- Set up consent update events
- Test with Tag Assistant for hours
- Wonder if it actually works
Just add the script and ship your feature.
Ready to add cookie consent to your Next.js app?