Platforms

How to add a cookie consent banner to a React single-page app

In a React single-page app, put the snippet in the HTML shell (index.html at the root of a Vite project, public/index.html in Create React App) above your app's script, not inside a component. Mark tracking tags in the same file, and gate embeds and route-change page views with a small hook that listens to Cookiequiet's cq:consent event.
Published 21 October 2026 · By the Cookiequiet team · Markdown

Steps

Copy the snippetIn the Cookiequiet dashboard, add the website and copy the snippet with your site key, data-consent-mode="1" and data-auto-block="1".
Paste it in the HTML shellOpen index.html (Vite) or public/index.html (Create React App) and paste the snippet in the head, above any other script and above the script that loads your app.
Mark your tagsPaste analytics and advertising tags below the snippet in the same file with type="text/plain" and data-cq-category set to analytics or marketing.
Add a useConsent hookCreate a hook that reads window.Cookiequiet.get() once and updates on the cq:consent event, then render embeds and widgets only when their category is granted.
Gate route-change page viewsIf you send page views on route changes with React Router, send them from an effect on useLocation only when analytics is granted.
Add the Cookie preferences linkIn the footer component, render a plain anchor with href="#cq-preferences" and the data-cq-show-preferences attribute.
Build and testBuild and preview the app, open it in a private window, click Reject all and check that no _ga or _fbp cookie is set. Accept all, reload, change routes and check the consent log.

Where does the snippet go in a React app?

A React single-page app has one HTML file that loads the bundle. The snippet goes in its head, above everything else. In a Vite project that file is index.html at the project root; in Create React App it is public/index.html. Create React App was deprecated by the React team in February 2025 and keeps working in maintenance mode, so the same step applies to existing apps.

html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cookiequiet.com/cq.js" data-site="%VITE_COOKIEQUIET_SITE%" data-consent-mode="1" data-auto-block="1" defer></script>
<title>My app</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

Vite replaces %VITE_COOKIEQUIET_SITE% in index.html with the variable from your .env file. If the variable is not defined, Vite leaves the text as it is, so check the built file, or write the key directly. Vite's entry script is a module script, which the browser runs after parsing, in document order with deferred scripts, so cq.js placed above it runs first.

Do not bundle cq.js or inject it from a component. The banner reads its settings from its own script tag and has to run before the tags it controls, which only the HTML shell guarantees.

Marked tags in the HTML shell. Paste them under the snippet and change their opening tag:

html
<script type="text/plain" data-cq-category="marketing">
/* Meta Pixel base code, unchanged */
</script>

Cookiequiet runs them when the category is granted and at once on later visits where the stored choice includes it. The Meta Pixel and Google Analytics 4 guides show the full tags.

Scripts your code adds at runtime. With data-auto-block="1", scripts and iframes created by JavaScript from about 80 known trackers are held until their category is granted. For anything else, gate it in your own code.

How do React components read the visitor's choice?

Cookiequiet dispatches a cq:consent event on window each time it applies a choice: a stored one on page load, a new one after a click, and every category in regions where geo-targeting does not show the banner. event.detail.categories lists the granted categories. window.Cookiequiet.get() returns the choice in force, whose c array holds the same list (with a: "geo" where no banner is shown), or null while no choice applies yet. A hook built on useSyncExternalStore:

tsx
// src/consent.ts
import { useSyncExternalStore } from "react";
​
const cq = (window as any).Cookiequiet;
let granted: string[] = cq?.get()?.c ?? [];
const listeners = new Set<() => void>();
​
window.addEventListener("cq:consent", (e) => {
granted = (e as CustomEvent).detail.categories;
listeners.forEach((l) => l());
});
​
export function useConsent(category: string) {
return useSyncExternalStore(
(cb) => {
listeners.add(cb);
return () => { listeners.delete(cb); };
},
() => granted.includes(category),
);
}

Render a YouTube player only when useConsent("marketing") is true; otherwise show a placeholder with a button that calls window.Cookiequiet.showPreferences().

What about route changes?

React Router and other client-side routers change the URL without reloading the page. The snippet runs once per full load, the choice stays applied and the banner does not reappear. Two things follow:

  • Marked scripts are activated on load and after each choice. Keep them in the HTML shell, not in components rendered later.
  • Route-change page views are your code's job. With React Router, run an effect on useLocation():
tsx
const location = useLocation();
const analytics = useConsent("analytics");
useEffect(() => {
if (analytics) sendPageView(location.pathname); // your analytics call
}, [location, analytics]);

If you use Next.js, Gatsby or a framework with server rendering, follow the Next.js guide or the matching framework page instead.

tsx
<a href="#cq-preferences" data-cq-show-preferences>Cookie preferences</a>

Cookiequiet handles clicks on this link anywhere in the page, so no click handler is needed.

How do I test it?

  1. Run the build and preview it (vite build then vite preview), then view the page source: cq.js comes first.
  2. Open the app in a private window and click Reject all. The cookies are cq_consent and your own, nothing from Google or Meta.
  3. Clear the cookies, click Accept all, reload: marked tags run. Change routes and check that the page views follow the choice.
  4. Open the consent log in the dashboard.

A Content Security Policy must allow https://cookiequiet.com in script-src and connect-src. Cookiequiet is free for one website with geo-targeting; Starter is €3 per website per month, with unlimited pageviews on every plan.

Questions

No. Cookiequiet is one script tag served from cookiequiet.com. It reads its settings from the attributes of its own tag and must run before your other scripts, so it belongs in index.html, not in the bundle.
No. A single-page app keeps the same document while the router changes the URL, so the script runs once and the stored choice stays applied.
Use a hook that reads window.Cookiequiet.get() and updates on the cq:consent event, and render the iframe only when the marketing category is granted. Show a placeholder with a button that opens the preferences otherwise.
Yes. Paste the snippet in public/index.html instead of index.html. The React team deprecated Create React App in February 2025, but existing apps keep working and the banner needs nothing from the build tool.

Put a quiet banner on your website today

Free for one website with every core feature, no card and no trial clock. Upgrade a website to Starter for $3 a month to match your brand with AI and drop the branding, or to Pro for $9 to design it with the AI agent.