npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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

  • version and expiryDays re-ask for consent when your categories change or a choice goes stale. Both default to the previous behaviour for existing stored consent.
  • storage accepts 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-banner
yarn add react-consent-management-banner
pnpm add react-consent-management-banner
bun add react-consent-management-banner

Peer 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" with aria-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-motion and prefers-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     # tsup

End-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   # interactive

To run the demo site against your local build:

npm run example:dev

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

License

MIT © Farasat Ali