# 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.

Source: https://cookiequiet.com/platforms/react
Published: 2026-10-21
Publisher: Cookiequiet

## Steps

1. **Copy the snippet** In the Cookiequiet dashboard, add the website and copy the snippet with your site key, data-consent-mode="1" and data-auto-block="1".
2. **Paste it in the HTML shell** Open 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.
3. **Mark your tags** Paste analytics and advertising tags below the snippet in the same file with type="text/plain" and data-cq-category set to analytics or marketing.
4. **Add a useConsent hook** Create 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.
5. **Gate route-change page views** If you send page views on route changes with React Router, send them from an effect on useLocation only when analytics is granted.
6. **Add the Cookie preferences link** In the footer component, render a plain anchor with href="#cq-preferences" and the data-cq-show-preferences attribute.
7. **Build and test** Build 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.

## How do I block my tags until consent?

**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](/platforms/meta-pixel) and [Google Analytics 4](/platforms/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](/platforms/nextjs) or the matching framework page instead.

## How do I add the Cookie preferences link?

```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

### Can I install Cookiequiet as an npm package?

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.

### Does the banner reappear on every route change?

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.

### How do I show a YouTube embed only after consent in React?

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.

### Does this work with Create React App?

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.

## Sources

- [React blog: Sunsetting Create React App (February 14, 2025)](https://react.dev/blog/2025/02/14/sunsetting-create-react-app)
- [Vite: Env variables and modes (HTML constant replacement)](https://vite.dev/guide/env-and-mode)
- [React: useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore)
- [React Router: useLocation](https://reactrouter.com/api/hooks/useLocation)
