June 29, 2026 · 8 min read · Updated June 29, 2026
Consent-Gated Analytics in Next.js (App Router): The Practical Guide
How to load analytics in Next.js only after a user consents, using the App Router, with the exact pattern and the pitfalls that break it.
By Tal Gerafi, Founder & Website Engineer
To make analytics consent-gated in Next.js (App Router), do not render your analytics component in layout.tsx by default. Store the user's choice in a cookie or localStorage, read it in a small Client Component, and conditionally render the analytics tag only when consent is granted. Until then, no analytics script touches the page. This is GDPR-friendly because under EU rules, optional analytics needs consent before any tracking cookie or request fires — not after.
The mistake we see most: the banner looks right, "Reject" works, but the analytics request already went out on first paint. Below is the pattern that actually holds, plus the two real strategies (hard-gate vs Google Consent Mode), route-change tracking, and the pitfalls that quietly leak data.
What does "consent-gated analytics" actually mean?
Consent-gated means the analytics script does not load, and no analytics network request fires, until the user has explicitly said yes. Loading the script first and "disabling" it after a reject is not consent-gated — the cookie or beacon already happened. Under GDPR and the ePrivacy Directive, non-essential analytics cookies require prior consent, so the gate has to sit in front of the script, not behind it.
There are two honest ways to build this in Next.js:
- Hard-gate — the GA4 / analytics component is simply not rendered until consent is
granted. Nothing loads, nothing fires. Simplest to reason about and the safest default for EU traffic. - Google Consent Mode v2 — the gtag script loads early but in a "denied" state (
ad_storage,analytics_storageset todenied), sends cookieless "pings", and switches tograntedwhen the user accepts. Google designed this so you keep modeled conversions while respecting the choice.
Most B2B sites we build pick the hard-gate because it is unambiguous and easy to verify. Consent Mode is the better fit when you run Google Ads and care about conversion modeling. We compare both below.
The Next.js App Router pattern (load analytics only after consent)
The whole pattern is three small pieces: a consent store, a banner that writes the choice, and a gate that reads it and conditionally renders analytics. Because consent lives in the browser, the gate must be a Client Component.
A minimal AnalyticsGate:
'use client'
import { useEffect, useState } from 'react'
import { GoogleAnalytics } from '@next/third-parties/google'
export function AnalyticsGate() {
const [consent, setConsent] = useState<'unknown' | 'granted' | 'denied'>('unknown')
useEffect(() => {
setConsent((localStorage.getItem('analytics-consent') as typeof consent) ?? 'unknown')
const onChange = () =>
setConsent((localStorage.getItem('analytics-consent') as typeof consent) ?? 'unknown')
window.addEventListener('consent-changed', onChange)
return () => window.removeEventListener('consent-changed', onChange)
}, [])
if (consent !== 'granted') return null
return <GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID!} />
}
Then render <AnalyticsGate /> in layout.tsx. Your banner's "Accept" handler writes localStorage.setItem('analytics-consent', 'granted') and dispatches a consent-changed event so the gate re-renders without a full reload. Because the gate returns null until consent is granted, @next/third-parties never injects the gtag script and GA4 never sees the user.
Two details people miss. First, start consent as unknown, not denied — on the server you can't read localStorage, so the gate must render nothing until the effect runs, which also avoids a hydration mismatch. Second, an <html suppressHydrationWarning>-style early return keeps server and client output identical (both render nothing) until the browser knows the answer.
This is the same supervised research → build → review loop Greeto uses on client sites: we verify the absence of the request before consent, not just the presence of a banner. The deeper "how" behind that workflow is in our guide on building production sites with Claude Code.
Hard-gate vs Google Consent Mode: which should you use?
Both are legitimate. The choice is about what you're optimizing for and how much Google tooling you depend on.
| Factor | Hard-gate (don't render until consent) | Google Consent Mode v2 |
|---|---|---|
| What loads before consent | Nothing | gtag loads in denied state, sends cookieless pings |
| Cookies before consent | None | None (storage denied) |
| EU compliance clarity | Very high — easy to prove zero tracking | High, but relies on Google's denied-state behavior |
| Conversion modeling (Google Ads) | Lost for pre-consent users | Preserved via modeled conversions |
| Setup complexity | Low | Medium (default + update gtag calls) |
| Easiest to verify | Yes — Network tab shows nothing | Harder — pings look like traffic |
| Best for | Most B2B/SaaS marketing sites | Sites running Google Ads at scale |
If you mostly need clean, trustworthy product analytics and a defensible privacy posture, hard-gate. If your paid acquisition depends on Google's conversion modeling, Consent Mode earns its extra wiring. You can also combine them: hard-gate everything except a Consent-Mode gtag that you load in denied state — but only do that if you genuinely use the modeled data, otherwise it's complexity for nothing.
How do you track route changes after consent in Next.js?
The App Router does not do full page loads on client navigation, so a single page_view on mount under-counts. After consent is granted, add a small Client Component that fires a page_view whenever the path or query changes, using usePathname and useSearchParams:
'use client'
import { useEffect } from 'react'
import { usePathname, useSearchParams } from 'next/navigation'
import { sendGAEvent } from '@next/third-parties/google'
export function PageViews() {
const pathname = usePathname()
const searchParams = useSearchParams()
useEffect(() => {
sendGAEvent('event', 'page_view', { page_path: pathname })
}, [pathname, searchParams])
return null
}
Render <PageViews /> inside your consent gate so it only runs after granted. Wrap it in <Suspense> if you use useSearchParams, since that hook opts the subtree into client rendering. Keep the event name and payload shape identical to a normal session — if late-consent users send a differently-shaped page_view, your reports fracture and you'll spend a day chasing a "data quality" ghost that is really a schema bug.
Pitfalls that quietly break consent gating
These are the ones that pass a casual look and fail a real audit:
- The script loads on first paint anyway. Someone put
<GoogleAnalytics>inlayout.tsxoutside the gate "just to be safe." Verify by checking the Network tab on a fresh visit with no choice made — you should see zerogoogle-analytics.comorgoogletagmanager.comrequests. - State and storage drift. Runtime React state says "denied" but a stale
localStoragekey says "granted" (or vice versa). Pick one source of truth, read it on mount, and broadcast changes with an event so every consumer stays in sync. - Route changes bypass the gate. Gating looks correct on first load, then a client-side navigation re-mounts something that loads analytics regardless. Test accept → navigate → reject → navigate, not just the landing page.
- Hydration mismatch from reading storage during render. Reading
localStoragein the render body (not inuseEffect) makes the server HTML and client HTML disagree. Default tounknown, decide in an effect. - No way to change your mind. Consent must be revocable. Give users a persistent "Cookie settings" link; in the EU, withdrawing consent has to be as easy as giving it.
- Re-loading the script on every grant. If your banner re-grants on each page, you can double-initialize GA. Guard initialization so it happens once per session.
We treat "is the request actually absent before consent?" as a human-in-the-loop gate, the same way we verify a real domain is serving the new build before calling a migration done. A banner that looks compliant and a network trace that proves it are different things.
FAQ
Can I collect anonymous analytics in Next.js before consent?
It depends on your jurisdiction and legal basis, but the safe default is no. Under GDPR and the ePrivacy Directive, non-essential analytics cookies and identifiers generally need prior consent. Some privacy-first, cookieless tools argue they need no consent — confirm that with your own counsel rather than assuming.
Do I need a cookie banner if I use Google Consent Mode?
Yes. Consent Mode is how you communicate the user's choice to Google's tags; it is not the consent mechanism itself. You still need a banner or preference UI where the user actually grants or denies, and Consent Mode reads that decision.
Where should I store the consent choice in Next.js?
A stable key in localStorage or a first-party cookie works well. Cookies are readable on the server (useful if you want to decide rendering server-side), while localStorage is client-only. Either way, keep one source of truth and read it on mount to avoid hydration mismatches.
Is GA4 still useful with strict consent gating?
Yes. You lose volume from users who decline, but the data you keep is cleaner and your privacy posture is defensible. For product decisions, trustworthy data from consenting users usually beats inflated counts you can't stand behind.
What breaks most often in the Next.js App Router specifically?
Two things: hydration timing (reading consent during render instead of in an effect) and client-side route changes that either skip page_view tracking or re-trigger script loading. Test navigation flows, not just the first page load.
Where this fits
Consent gating is one slice of getting a Next.js marketing site right end to end. If you're moving off WordPress, analytics and consent should be wired during the rebuild, not bolted on after — see the WordPress to Next.js migration and SEO guide. And if you're tightening up how the same site shows up in AI search, the measurement that consent protects feeds directly into that work; our practical AEO playbook for B2B websites covers the answer-first side. Done properly, consent-gated analytics is not a banner component — it's a behavior contract across scripts, state, and events, and it earns trust without costing you decision-grade data.