react-consent-management-banner
v2.0.0
Published
Beautiful and Highly Customizable GDPR/ePrivacy React Consent and Cookie Management Library for React and Next.js
Maintainers
Keywords
Readme
Upgrading from 1.x
2.0.0 rewrites the banner and modal for accessibility, responsiveness and
theming. <CookieConsent GA_TRACKING_ID=… /> keeps its signature, and stored
consent from 1.x is still honoured — visitors are not re-prompted on upgrade.
Three things will affect you.
Custom CSS needs remapping
Every class was renamed:
| 1.x | 2.0 |
| --- | --- |
| .cookie-banner-wrapper | .ccb-wrapper |
| .cookie-banner | .ccb-banner |
| .cookie-buttons | .ccb-banner__actions |
| .btn-outline | .ccb-btn--ghost |
| .cookie-modal | .ccb-modal |
| .cookie-modal-content | .ccb-modal__panel |
| .cookie-modal__btn | .ccb-modal__actions |
| .cookie-modal__close | .ccb-modal__close |
| .cookie-settings-button | .ccb-fab |
| .top / .bottom | .ccb-wrapper--top / .ccb-wrapper--bottom |
| .top-left … .bottom-right | .ccb-fab--top-left … .ccb-fab--bottom-right |
The four --cookie-consent-* custom properties are unchanged, so themes built
on those keep working.
Button text colour is chosen automatically
Button text stays white unless white fails the WCAG AA contrast minimum
against your buttonBackgroundColor, in which case it switches to black. If
you relied on white text over a light accent, set it explicitly:
config={{ buttonBackgroundColor: "#6aa9ff", buttonTextColor: "#ffffff" }}Only real Consent Mode keys reach gtag
Custom categories are no longer forwarded to gtag("consent", …) — previously
any option key was passed through as a consent signal, including ones Google
does not recognise. Act on your own categories via onPreferencesChange.
New, optional
versionandexpiryDaysre-ask for consent when your categories change or a choice goes stale. Both default to the previous behaviour for existing stored consent.storageaccepts a cookie-backed implementation, for consent shared across subdomains.GA_TRACKING_ID={null}sets Consent Mode without injecting gtag, for hosts that manage it themselves.
Why
Most consent banners either look like a default Bootstrap alert or cost a
subscription. This one is a single component: it renders the banner and the
preferences modal, persists the visitor's choice, loads gtag for you, and
keeps Google Consent Mode v2 in sync — with eight consent categories out of the
box.
Installation
npm install react-consent-management-banneryarn add react-consent-management-banner
pnpm add react-consent-management-banner
bun add react-consent-management-bannerPeer dependency: react >= 17.
Quick start
import { CookieConsent } from "react-consent-management-banner";
import "react-consent-management-banner/style.css";
export default function Layout({ children }) {
return (
<>
{children}
<CookieConsent GA_TRACKING_ID="G-XXXXXXXXXX" />
</>
);
}You do not need to add the gtag snippet yourself. The component sets the
denied-by-default consent state first, then injects Google's script — so
nothing is measured before the visitor has chosen.
Already manage gtag yourself? Pass GA_TRACKING_ID={null} and the component
will set Consent Mode state without injecting anything.
Next.js App Router: the package ships the
"use client"directive.
Consent Mode v2
Before any interaction:
gtag("consent", "default", {
ad_storage: "denied",
ad_personalization: "denied",
ad_user_data: "denied",
analytics_storage: "denied",
personalization_storage: "denied",
functionality_storage: "granted",
security_storage: "granted",
});Then a consent update once a choice is saved. Only keys Consent Mode
actually understands are forwarded — your own custom categories are yours to
act on via onPreferencesChange.
Layout & position
| layout | position | Result |
| --- | --- | --- |
| "bar" (default) | "bottom" / "top" | Full-width bar, horizontal above 880px |
| "card" | "bottom-right", "bottom-left", "top-right", "top-left" | Compact 420px corner panel |
<CookieConsent
GA_TRACKING_ID="G-XXXXXXXXXX"
config={{ banner: { layout: "card", position: "bottom-right" } }}
/>On screens under 600px every layout collapses to a bottom sheet — full width, top-rounded, safe-area aware, with stacked full-width buttons. Corner cards are unusable at phone widths.
Re-asking for consent
Two mechanisms, both important for staying compliant:
<CookieConsent
GA_TRACKING_ID="G-XXXXXXXXXX"
config={{
version: 2, // bump whenever you change your categories
expiryDays: 365, // re-ask at least annually
}}
/>| Option | Default | Why |
| --- | --- | --- |
| version | 1 | A choice made against older categories does not cover new ones. Bumping it treats stored consent as absent, so visitors are asked again rather than silently carrying consent they never gave. |
| expiryDays | 365 | Supervisory authorities generally expect consent to be refreshed at least annually. |
Consent written by earlier releases of this package (a bare preferences map) is still honoured, so upgrading does not re-prompt everyone.
Storage
Defaults to localStorage, wrapped so blocked storage (Safari private mode,
strict privacy settings) degrades to "not persisted" rather than throwing.
localStorage is not shared across subdomains. If you need one choice to
cover www. and app., supply a cookie-backed store:
const cookieStorage = {
getItem: (k) =>
document.cookie.match(new RegExp(`(^| )${k}=([^;]+)`))?.[2] ?? null,
setItem: (k, v) => {
document.cookie = `${k}=${v};domain=.example.com;path=/;max-age=31536000;SameSite=Lax`;
},
removeItem: (k) => {
document.cookie = `${k}=;domain=.example.com;path=/;max-age=0`;
},
};
<CookieConsent GA_TRACKING_ID="G-XXXX" config={{ storage: cookieStorage }} />;| Option | Type | Default | Description |
| --- | --- | --- | --- |
| storage | ConsentStorage | localStorage | { getItem, setItem, removeItem }. |
| storageKey | string | "cookiePreferences" | Key used for persistence. |
Configuration
config is deep-merged onto the defaults one level down, so overriding a
single field keeps its siblings:
// Keeps the default buttons, links and all eight categories.
<CookieConsent
GA_TRACKING_ID="G-XXXXXXXXXX"
config={{ banner: { title: "We use cookies." } }}
/>| Option | Type | Description |
| --- | --- | --- |
| banner.title | string | Banner copy. |
| banner.position | BannerPosition | See layout table above. |
| banner.layout | "bar" \| "card" | Shape. |
| banner.button.* | string | Accept / reject / preferences labels. |
| banner.links.* | { title, url } | Cookie policy, privacy policy, terms, plus moreLinks[]. |
| preferences.title / para | string | Modal copy. |
| preferences.options | IPreferenceOption[] | Your consent categories. |
| preferences.closeLabel | string | Accessible label for the close button. |
| cookieFloatingButton.show | boolean | Re-open button after a choice is made. |
| cookieFloatingButton.position | corner | Where it sits. |
| cookieFloatingButton.Component | ComponentType<SVGProps> | Your own icon. |
| cookieFloatingButton.label | string | Its accessible name. |
| colorScheme | "auto" \| "light" \| "dark" | auto follows prefers-color-scheme. |
| zIndex | number | Stacking order. Default 99999. |
| onPreferencesChange | (prefs, consentGiven) => void | Fires on every change. |
Reading consent back
const config = {
onPreferencesChange: (prefs, consentGiven) => {
if (prefs.analytics_storage) startAnalytics();
},
};
<CookieConsent GA_TRACKING_ID="G-XXXX" config={config} />;
// config.getConsentGiven() and config.getConsentPreferences() are populated
// on the object you passed in.Custom categories
<CookieConsent
GA_TRACKING_ID="G-XXXXXXXXXX"
config={{
version: 2, // bump, because the categories changed
preferences: {
title: "Your choices",
button: { savePreferencesText: "Save", goBackText: "Cancel" },
options: [
{ key: "necessary_storage", label: "Essential", alwaysEnabled: true, description: "Required for the site to work." },
{ key: "analytics_storage", label: "Analytics", description: "Helps us improve the site." },
],
},
}}
/>Theming
Four public custom properties drive everything else:
:root {
--cookie-consent-background-color: #fff;
--cookie-consent-text-color: #000;
--cookie-consent-link-color: #6ac3ff;
--cookie-consent-button-background-color: #0073e6;
}Or via config:
<CookieConsent
GA_TRACKING_ID="G-XXXX"
config={{
colorScheme: "dark",
backgroundColor: "#131a26",
textColor: "#e8eef8",
buttonBackgroundColor: "#6aa9ff",
}}
/>Button text is chosen for you
Button text defaults to white, and automatically flips to black when white
would fall below the WCAG AA contrast minimum of 4.5:1 against your accent —
so a light buttonBackgroundColor cannot silently produce unreadable buttons.
Override it explicitly when you want to:
config={{ buttonBackgroundColor: "#6aa9ff", buttonTextColor: "#04101f" }}| Option | Type | Default | Description |
| --- | --- | --- | --- |
| buttonTextColor | string | auto | Text on primary buttons. Unset means "pick whichever of black/white meets AA". |
Accessibility
- The banner is a labelled
role="region", so screen-reader users can find it. - The preferences modal is a real
role="dialog"witharia-modal, labelled by its heading. - Focus is trapped in the modal and restored on close; Escape closes it.
- Background scrolling is locked while the modal is open.
- Every control is a real
<button>or<input>, keyboard operable, with visible focus rings. - Honours
prefers-reduced-motionandprefers-color-scheme.
Styling
import "react-consent-management-banner/style.css";| Class | Element |
| --- | --- |
| .ccb-wrapper | Positioning shell |
| .ccb-banner | Banner surface |
| .ccb-btn--primary / .ccb-btn--ghost | Buttons |
| .ccb-modal / .ccb-modal__panel | Preferences dialog |
| .ccb-option | One consent category |
| .ccb-fab | Floating re-open button |
Disclaimer
This component gives you the mechanics of collecting and honouring consent. It is not legal advice, and shipping it does not by itself make a site GDPR compliant — that depends on what you actually do with the data.
Contributing
Issues and pull requests are welcome.
git clone https://github.com/faraasat/react-consent-management-banner.git
cd react-consent-management-banner
npm install
npm test # vitest unit tests
npm run typecheck # tsc --noEmit
npm run build # tsupEnd-to-end tests run against the built demo in a real browser (desktop and mobile viewports), and cover the things unit tests cannot: layout, CSS and keyboard behaviour.
npm run build && npm --prefix example install && npm --prefix example run build
npm run test:e2e # playwright
npm run test:e2e:ui # interactiveTo run the demo site against your local build:
npm run example:devReleases are manual — nothing publishes on a push to main. Maintainers run
the Release workflow from the Actions tab.
Privacy
The published package contains no telemetry. The demo site at faraasat.github.io/react-consent-management-banner uses Google Analytics and Aptabase; the library itself never phones home.
