Keep the visitor's choice in React state, save it to a first-party cookie or browser storage, and render analytics or ad scripts only when the matching category is granted. A small hook plus one banner component covers it. If you prefer a package, react-cookie-consent gives a ready banner with props for decline buttons, cookie name and expiry.
| Build it yourself | react-cookie-consent package | |
|---|---|---|
| Categories (analytics, ads) | Yes, any you need | One accept or decline value by default |
| Reject button | Yes | Off by default; enableDeclineButton turns it on |
| Where the choice is stored | Your choice of cookie or storage | A cookie named CookieConsent by default |
| Default expiry | You set it | 365 days |
| Licence | Your own code | MIT |
Updated
The hard part of a consent banner is not the box on screen, it is making sure nothing non-essential runs before the visitor says yes. In React that is a state problem: one source of truth for the choice, and components that read it before injecting a script tag. The live demo below does exactly that, so you can watch the analytics loader stay off until you accept.
Pretend website
We use cookies for analytics and ads only if you agree.
Reject all Accept all Choose
No choice yet, so nothing optional loads.
None yet.
Cookie settings
The demo remembers your choice in this browser only. Nothing is sent anywhere.
Three small pieces. They use no library, run in any React 18 or 19 app, and match the demo above.
Reads the saved choice once on the client, and saves plus broadcasts every change.
import { useEffect, useState } from "react";
const KEY = "consent-v1";
export function useConsent() {
const [consent, setConsent] = useState(null); // null = not chosen yet
useEffect(() => {
try {
const saved = localStorage.getItem(KEY);
if (saved) setConsent(JSON.parse(saved));
} catch {}
}, []);
function save(next) {
setConsent(next);
try { localStorage.setItem(KEY, JSON.stringify(next)); } catch {}
if (typeof window.gtag === "function") {
window.gtag("consent", "update", {
analytics_storage: next.analytics ? "granted" : "denied",
ad_storage: next.ads ? "granted" : "denied",
ad_user_data: next.ads ? "granted" : "denied",
ad_personalization: next.ads ? "granted" : "denied",
});
}
}
function reset() {
setConsent(null);
try { localStorage.removeItem(KEY); } catch {}
}
return { consent, save, reset };
}Accept and Reject sit side by side; category boxes start unticked.
import { useState } from "react";
export function ConsentBanner({ consent, save }) {
const [open, setOpen] = useState(false);
const [pick, setPick] = useState({ analytics: false, ads: false });
if (consent) return null;
return (
<div role="dialog" aria-label="Cookie consent" className="cookie-banner">
<p>We use cookies for analytics and ads only if you agree.</p>
{open && (
<fieldset>
<label><input type="checkbox" checked disabled /> Necessary</label>
<label><input type="checkbox" checked={pick.analytics}
onChange={(e) => setPick({ ...pick, analytics: e.target.checked })} /> Analytics</label>
<label><input type="checkbox" checked={pick.ads}
onChange={(e) => setPick({ ...pick, ads: e.target.checked })} /> Advertising</label>
<button onClick={() => save(pick)}>Save choices</button>
</fieldset>
)}
<button onClick={() => save({ analytics: false, ads: false })}>Reject all</button>
<button onClick={() => save({ analytics: true, ads: true })}>Accept all</button>
<button onClick={() => setOpen(true)}>Choose</button>
</div>
);
}The script tag is created only when the category is granted, so a refusal means it never exists.
import { useEffect } from "react";
export function AnalyticsLoader({ allowed, src }) {
useEffect(() => {
if (!allowed) return;
const s = document.createElement("script");
s.src = src;
s.async = true;
document.head.appendChild(s);
}, [allowed, src]);
return null;
}
// In your root layout:
// const { consent, save } = useConsent();
// <AnalyticsLoader allowed={!!consent?.analytics} src="https://example.com/analytics.js" />
// <ConsentBanner consent={consent} save={save} />Install it with npm install react-cookie-consent. These are the props and helpers its README documents, with their defaults, as of October 2026.
| Prop or helper | Default | What it does |
|---|---|---|
| location | "bottom" | Where the bar sits: top, bottom or none |
| buttonText | "I understand" | Accept button text |
| enableDeclineButton | false | Shows a decline button |
| declineButtonText | "I decline" | Decline button text |
| cookieName | "CookieConsent" | Name of the cookie that stores the answer |
| expires | 365 | Days until the cookie expires |
| onAccept / onDecline | () => {} | Run your code when the visitor answers |
| getCookieConsentValue(name) | Reads the stored answer | |
| resetCookieConsentValue() | Removes the stored answer so the bar shows again |
Accept loads analytics; Decline does nothing optional. The check on first load covers returning visitors.
import CookieConsent, { getCookieConsentValue } from "react-cookie-consent";
import { useEffect } from "react";
function loadAnalytics() {
const s = document.createElement("script");
s.src = "https://example.com/analytics.js";
s.async = true;
document.head.appendChild(s);
}
export default function App() {
useEffect(() => {
if (getCookieConsentValue("site-consent") === "true") loadAnalytics();
}, []);
return (
<CookieConsent
cookieName="site-consent"
buttonText="Accept"
enableDeclineButton
declineButtonText="Reject"
onAccept={loadAnalytics}
>
We use analytics cookies only if you agree.
</CookieConsent>
);
}
A banner that only hides itself after Reject, while the analytics snippet in index.html has already run, records the visit anyway. Moving the snippet into a component that renders only after consent is the fix.
For tags you cannot move, such as ones a platform injects for you, use that platform's consent setting or Consent Mode so the tag runs in its restricted state until the update arrives.
Open the site in a private window, open your browser's developer tools and check the Network panel: before you press anything, no analytics or ad requests should appear. Then accept and confirm they start.
Repeat with Reject all and reload a second page to confirm the refusal is remembered.
No. Its README describes a banner that stores the visitor's answer in a cookie and calls onAccept or onDecline. Loading or skipping your analytics is still your job, for example by checking getCookieConsentValue before you add the script.
Set the enableDeclineButton prop; it is false by default. The button text defaults to I decline and can be changed with declineButtonText, and onDecline runs when it is pressed.
With your own hook, clear the saved value and the banner returns. With the package, call resetCookieConsentValue, passing the same cookie name you gave the banner if you changed it from the default.
Either works for the browser. A cookie can also be read by your server, which helps if the server decides which tags to print. Browser storage keeps the value out of every request. Both are allowed for remembering the choice itself.
Put the gtag consent default in the HTML head before any Google tag, outside React, so it runs first. Call gtag('consent', 'update', ...) from your Accept handler inside React.
Related: Cookie consent banner examples, Cookie policy generator, Privacy policy generator, GDPR statement generator.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.