React & Next.js Cookie Banner: GDPR Consent Implementation Guide (2026)
Building a GDPR-compliant cookie consent banner in React has two hard parts: blocking third-party scripts before consent fires, and persisting consent state without breaking SSR. Here's how to do both correctly.
Why Cookie Consent Is Harder in React Apps
In a traditional server-rendered website, adding a cookie banner is straightforward — you inject a script into the HTML head. In React and Next.js apps, several challenges arise:
- Script loading order — Google Analytics and Meta Pixel often load through Next.js's
<Script>component or third-party integrations, making them harder to conditionally block. - SSR hydration — Reading
localStorageordocument.cookieon the server causes hydration mismatches. Consent state must be handled client-side only. - Third-party tag managers — Google Tag Manager, Segment, and similar tools load scripts automatically; GDPR requires you to prevent this until consent is given.
- App Router vs Pages Router — Next.js 13+ App Router changes how scripts and providers are structured, requiring different integration patterns.
Two Approaches: Build vs. Generate
A generated, self-contained JavaScript file that manages consent without React dependencies. Loads synchronously, blocks scripts immediately, works in any React framework. Zero bundle impact.
A React component using useState/useEffect for consent state. More idiomatic for React apps but requires careful SSR handling and careful script-load coordination.
For most production apps, the vanilla JS approach (Option A) is more reliable. It loads before React hydrates, prevents any flash of unblocked scripts, and doesn't add to your bundle. The generated cookie banner from our kit uses this approach.
The GDPR Requirements Your Banner Must Meet
- ✓Block scripts before consent — Analytics and marketing scripts must not execute until the user explicitly accepts
- ✓Equal accept/reject prominence — Rejecting must be as easy as accepting. No hiding the reject button
- ✓Granular categories — At minimum: essential (always on), analytics, marketing
- ✓Persist consent decision — Don't re-ask on every page load. Store decision in localStorage or a first-party cookie
- ✓Revocable at any time — Users must be able to change their consent decision after initial choice
- ✗Don't use pre-ticked checkboxes or dark patterns to push users toward acceptance
- ✗Don't load Google Analytics before consent fires — even briefly
Integrating the Generated Banner in Next.js (App Router)
The generated vanilla JS banner from our kit is added as an inline script in your root layout. For Next.js App Router:
app/layout.tsximport Script from 'next/script'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
{/* Cookie banner loads synchronously before React hydrates */}
<Script
src="/cookie-banner.js"
strategy="beforeInteractive"
/>
</head>
<body>
{children}
</body>
</html>
)
}
Why strategy="beforeInteractive"? This tells Next.js to load the script before React hydration begins — ensuring consent state is read from localStorage and tracking scripts are blocked before any analytics code can run. It's the only strategy that reliably prevents a consent window where scripts could briefly execute.
Conditionally Loading Analytics After Consent
With the generated banner in place, your analytics scripts should only load after consent is granted. Here's the pattern for Google Analytics in Next.js:
components/Analytics.tsx'use client'
import Script from 'next/script'
import { useEffect, useState } from 'react'
export function Analytics() {
const [analyticsConsent, setAnalyticsConsent] = useState(false)
useEffect(() => {
// Read consent from localStorage (set by the cookie banner)
const consent = localStorage.getItem('cookie-consent')
if (consent) {
const parsed = JSON.parse(consent)
setAnalyticsConsent(parsed.analytics === true)
}
// Listen for consent updates
const handler = (e: Event) => {
const detail = (e as CustomEvent).detail
setAnalyticsConsent(detail.analytics === true)
}
window.addEventListener('cookieConsentUpdated', handler)
return () => window.removeEventListener('cookieConsentUpdated', handler)
}, [])
if (!analyticsConsent) return null
return (
<>
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GA_ID}`}
strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${process.env.NEXT_PUBLIC_GA_ID}');
`}
</Script>
</>
)
}
Then add <Analytics /> to your root layout. The component renders nothing until analytics consent is granted, at which point it loads the Google Analytics script.
Pages Router Integration
For Next.js Pages Router (pages/_app.tsx):
import type { AppProps } from 'next/app'
import Head from 'next/head'
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Head>
{/* Inline the banner script directly for Pages Router */}
<script src="/cookie-banner.js" />
</Head>
<Component {...pageProps} />
</>
)
}
Plain React (Vite / Create React App)
For React apps not using Next.js, add the script to your index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My App</title>
<!-- Cookie banner loads before React bundle -->
<script src="/cookie-banner.js"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Hydration Safety: Avoiding SSR Mismatches
If you build a React cookie consent component, you'll run into a common SSR issue: the server renders with no consent state, while the client reads from localStorage and renders the banner differently. This causes a hydration mismatch warning (or silent visual bug).
The solution is to suppress the banner rendering on the initial server render:
components/CookieBanner.tsx'use client'
import { useState, useEffect } from 'react'
export function CookieBanner() {
// Start as null (no render) to match server output
const [consent, setConsent] = useState<string | null>(null)
useEffect(() => {
// Only runs on client — safe to read localStorage
setConsent(localStorage.getItem('cookie-consent'))
}, [])
// Don't render anything until client hydration completes
if (consent === null) return null
// Don't show banner if user has already chosen
if (consent !== '') return null
return (
<div className="cookie-banner">
{/* Banner UI */}
</div>
)
}
Your cookie banner must link to your privacy policy before users give consent. Clicking "Accept" without being able to review what they're consenting to does not constitute valid GDPR consent. Make sure your banner includes a "Privacy Policy" link.
Testing Your Cookie Consent Implementation
Before going live, verify your implementation with these checks:
- Open your site in a fresh private/incognito window
- Open DevTools Network tab and filter for
google-analytics.comandfacebook.com - Verify zero requests to tracking domains before you click "Accept"
- Click "Accept" — verify analytics requests begin
- Reload the page — verify the banner does not re-appear
- Find your "Cookie preferences" link (footer or banner) and click "Reject all"
- Reload — verify analytics requests stop
You can also use the Cookie Consent Check tool or browser extensions like "EditThisCookie" to inspect what's being set.
Get the generated cookie banner + full compliance pack
The Compliance Starter Pack generates a production-ready vanilla JS cookie banner, privacy policy, and terms of service tailored to your jurisdiction. Drop it into any React or Next.js app in minutes.
Generate Developer Pack — $6.99This guide is for informational purposes only and does not constitute legal advice. Framework APIs change frequently; verify against current Next.js and React documentation. For complex compliance requirements, consult a qualified data protection attorney.