Tutorial · September 29, 2026 · 5 min read
How to add a cookie consent banner to a Next.js site
To add a cookie consent banner to Next.js, install ReactFrame's free Cookie Consent Banner with npx shadcn@latest add @reactframe/cookie-consent-banner, render it from a small client component in your root layout, and load analytics only when onConsentChange reports that category as accepted. Re-apply the saved choice from localStorage on later visits.
A cookie banner is only useful if it actually controls what runs. The common mistake is a banner that looks compliant while Google Analytics or an ad pixel loads on the first page view anyway. This tutorial builds one that waits for the visitor's answer, using ReactFrame's free Cookie Consent Banner.
1. Install
npx shadcn@latest add @reactframe/cookie-consent-banner
The banner shows "Accept all", "Reject all" and a settings view where each category has its own switch. The visitor's choice is saved in localStorage, so it doesn't come back on every page.
2. Decide your categories
The defaults are Strictly necessary (always on), Analytics, Marketing and Preferences. Pass your own list to match the tools you actually use:
const categories = [
{ id: "necessary", label: "Essential", description: "Sign-in, cart and security.", required: true },
{ id: "analytics", label: "Analytics", description: "Anonymous usage statistics." },
{ id: "marketing", label: "Marketing", description: "Ad measurement and retargeting." },
];
required: true locks a category on. Leave defaultEnabled off for anything optional: in the EU, pre-ticked boxes don't count as consent.
3. Wrap it in a client component
onConsentChange is a function, and a Server Component (like app/layout.tsx) can't pass functions to a Client Component. Put the banner and your consent logic in one small client file:
// components/consent-manager.tsx
"use client";
import * as React from "react";
import { CookieConsentBanner, type CookieConsent } from "@/components/cookie-consent-banner";
declare global {
interface Window {
gtag?: (...args: unknown[]) => void;
}
}
function applyConsent(consent: CookieConsent) {
window.gtag?.("consent", "update", {
analytics_storage: consent.analytics ? "granted" : "denied",
ad_storage: consent.marketing ? "granted" : "denied",
ad_user_data: consent.marketing ? "granted" : "denied",
ad_personalization: consent.marketing ? "granted" : "denied",
});
}
export function ConsentManager() {
// The banner reports a choice when the visitor makes one; on later visits, re-apply the saved one.
React.useEffect(() => {
try {
const saved = JSON.parse(localStorage.getItem("cookie-consent") ?? "null");
if (saved) applyConsent(saved);
} catch {}
}, []);
return <CookieConsentBanner policyHref="/cookies" onConsentChange={applyConsent} />;
}
Then render <ConsentManager /> once, inside <body> in app/layout.tsx.
4. Default everything to "denied"
With Google Consent Mode, tags start in a denied state and only switch on after the update above. Set the default before the Google tag loads:
// app/layout.tsx (import Script from "next/script"), before the Google tag
<Script id="consent-default" strategy="beforeInteractive">
{`window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
analytics_storage: 'denied', ad_storage: 'denied',
ad_user_data: 'denied', ad_personalization: 'denied'
});`}
</Script>
For scripts that don't support Consent Mode (most ad pixels, chat widgets, heatmaps), don't load them at all until their category is true: keep the consent in React state and render their <Script> conditionally.
5. Let people change their mind
showReopen (on by default) leaves a small button that reopens the settings after a choice. Link to the same settings from your cookie policy page too. If you change your categories later, set a new storageKey (for example "cookie-consent-v2") so everyone is asked again.
Useful props
| Prop | Default | What it does |
|---|---|---|
variant | "card" | "card" floats in a corner, "bar" spans the bottom. |
position | "left" | Corner for the card. |
delay | 0.6 | Seconds before it slides in. |
storageKey | "cookie-consent" | Where the choice is saved. |
theme | "dark" | "dark" or "light". |
This is the interface and the wiring, not legal advice: which categories you need and how you word them depends on the tools you use and where your visitors are.

Ahmet Loca · Product designer
Hi, I'm Ahmet. I have over four years of experience creating intuitive, user-focused designs for digital products, and ReactFrame is where that work becomes components you can use in your own.