13 KiB
@pdntechnology/cookie-consent
A lightweight, open-source Consent Management Platform (CMP) for React and Next.js that implements Google Consent Mode v2 the same way certified tools do.
No heavy dependencies. No vendor lock-in. Works with Google Tag Manager, GA4, Google Ads, and Meta Pixel.
Why this exists
Google-certified CMPs (Cookiebot, OneTrust, CookieYes, …) follow the same technical flow:
- Set denied defaults before any Google tag loads
- Load GTM / gtag.js
- Show a consent banner
- On user choice →
gtag('consent', 'update', …)+ dataLayer event - When ads are denied → enable
ads_data_redactionandurl_passthrough
This library implements that flow in ~15 KB of readable JavaScript.
Note: Certified CMPs additionally provide IAB TCF v2.2 signals and appear on Google's certified vendor list. This library covers the Consent Mode v2 technical integration. You are responsible for privacy policy text and legal compliance.
Features
- Google Consent Mode v2 (all 7 signals)
- Granular categories: Necessary / Analytics / Marketing
- Customizable banner text, theme colors, and cookie inventory
ads_data_redaction+url_passthroughin bootstrap and on update- Regional default consent (EEA, etc.)
- React banner + Next.js bootstrap component
- Optional GTM and Meta Pixel loaders (consent-gated)
- Framework-agnostic core API (vanilla JS supported)
- TypeScript definitions for core API and React components
- Error callbacks when consent cannot be saved
Turkish documentation: README.tr.md
Installation
npm install @pdntechnology/cookie-consent
Next.js setup
Add to next.config.mjs:
const nextConfig = {
transpilePackages: ["@pdntechnology/cookie-consent"],
};
Quick start
1. Configure (optional)
// lib/consent-config.js
import { configureConsent } from "@pdntechnology/cookie-consent";
configureConsent({
storageKey: "my_site_consent", // localStorage key
waitForUpdateMs: 500, // time GTM waits for consent update
adsDataRedaction: true, // Google recommendation when ads denied
urlPassthrough: true, // pass click IDs in URL when cookies denied
debug: false,
});
Import this file in your root layout before ConsentBootstrap.
2. Bootstrap consent defaults (before GTM)
// app/layout.jsx
import "@pdntechnology/cookie-consent/styles.css";
import "@/lib/consent-config";
import { ConsentBootstrap } from "@pdntechnology/cookie-consent/react";
export default function RootLayout({ children }) {
return (
<html>
<body>
<ConsentBootstrap />
{children}
</body>
</html>
);
}
3. Banner + tags
// app/providers.jsx
import {
CookieConsent,
GoogleTagManager,
FacebookPixel,
} from "@pdntechnology/cookie-consent/react";
export function Providers({ children }) {
return (
<>
{children}
<CookieConsent
policyHref="/privacy"
labels={{
accept: "Accept all",
reject: "Reject all",
manage: "Manage preferences",
}}
/>
<GoogleTagManager gtmId="GTM-XXXXXXX" />
<FacebookPixel pixelId="123456789" />
</>
);
}
4. Re-open preferences (footer link)
import { openPreferences } from "@pdntechnology/cookie-consent";
<button type="button" onClick={openPreferences}>
Cookie settings
</button>
Customization
Text and policy URL
Every visible string can be overridden via the labels prop. The policy link uses policyHref.
<CookieConsent
policyHref="/privacy"
labels={{
title: "Cookie preferences",
message: "We use cookies to improve your experience.",
policyLink: "Privacy policy",
accept: "Accept all",
reject: "Reject all",
manage: "Manage preferences",
save: "Save preferences",
necessaryTitle: "Necessary",
necessaryDescription: "Required for core site functionality.",
analyticsTitle: "Analytics",
analyticsDescription: "Google Analytics (_ga, _gid).",
marketingTitle: "Marketing",
marketingDescription: "Google Ads and Meta Pixel.",
alwaysOn: "Always on",
storageError: "Your preferences could not be saved. Please try again.",
}}
/>
| Label key | Default |
|---|---|
title |
Cookie preferences |
message |
We use cookies to improve your experience… |
policyLink |
Privacy policy |
accept |
Accept all |
reject |
Reject all |
manage |
Manage preferences |
save |
Save preferences |
necessaryTitle |
Necessary |
necessaryDescription |
Required for core site functionality… |
analyticsTitle |
Analytics |
analyticsDescription |
Helps us understand how visitors use the site… |
marketingTitle |
Marketing |
marketingDescription |
Used for ads and remarketing… |
alwaysOn |
Always on |
cookiesHeading |
View cookies |
cookieName |
Name |
cookiePurpose |
Purpose |
cookieDuration |
Duration |
cookieProvider |
Provider |
storageError |
Your preferences could not be saved… |
Theme colors
Pass a theme object to override CSS variables without writing custom CSS:
<CookieConsent
theme={{
background: "rgba(15, 23, 42, 0.97)",
text: "#f8fafc",
textMuted: "rgba(248, 250, 252, 0.82)",
link: "#93c5fd",
primary: "#2563eb",
primaryText: "#ffffff",
secondaryBackground: "rgba(255, 255, 255, 0.1)",
border: "rgba(255, 255, 255, 0.14)",
accent: "#2563eb",
}}
/>
You can also pass className for additional styling hooks.
Cookie inventory table
Provide a cookies array to show an expandable table inside each category in the preferences panel:
<CookieConsent
cookies={[
{
category: "necessary",
name: "my_site_consent",
purpose: "Stores your consent choice",
duration: "1 year",
provider: "This site",
},
{
category: "analytics",
name: "_ga",
purpose: "Visitor statistics",
duration: "2 years",
provider: "Google Analytics",
},
]}
/>
If cookies is omitted, the banner keeps the simpler three-category view.
Error handling
Consent write functions return a result object:
const result = acceptAll();
if (!result.ok) {
console.error(result.reason); // "invalid" | "storage"
}
In React, use callbacks on the banner:
<CookieConsent
onConsentSaved={(consent) => console.log("saved", consent)}
onConsentError={({ reason }) => console.warn("failed", reason)}
/>
Enable debug: true in configureConsent() to log warnings to the console, or use onStorageError for global handling.
Example app
See examples/next-app for a working Next.js demo with Turkish labels, custom theme colors, and a cookie inventory table.
cd examples/next-app
npm install
npm run dev
| Banner category | User choice | Google Consent Mode signals |
|---|---|---|
| Necessary | Always on | security_storage: granted |
| Analytics | Opt-in | analytics_storage |
| Marketing | Opt-in | ad_storage, ad_user_data, ad_personalization, functionality_storage, personalization_storage |
When marketing is denied, the library also sets:
ads_data_redaction: trueurl_passthrough: true
Project structure
src/
├── index.js Public API
├── config.js configureConsent()
├── constants.js Events, signal names
├── preferences.js Parse / serialize user choices
├── storage.js localStorage read/write
├── consent-api.js acceptAll, rejectAll, getConsent, …
├── cookie-schema.js Cookie inventory helpers
├── google/
│ ├── gtag.js dataLayer helpers
│ ├── consent-signals.js Category → signal mapping
│ ├── consent-lifecycle.js default + update calls
│ └── bootstrap-script.js Inline <head> script generator
└── react/
├── ConsentBootstrap.jsx
├── CookieConsent.jsx
├── CookieList.jsx
├── theme.js
├── GoogleTagManager.jsx
└── FacebookPixel.jsx
API reference
Configuration
| Function | Description |
|---|---|
configureConsent(config) |
Set storage key, wait time, region defaults |
getConsentConfig() |
Read active configuration |
User consent
| Function | Description |
|---|---|
getConsent() |
Read stored preferences |
setConsent({ analytics, marketing }) |
Save custom choice; returns { ok, consent | reason } |
acceptAll() |
Grant all categories; returns { ok, consent | reason } |
rejectAll() |
Deny optional categories; returns { ok, consent | reason } |
hasAnalyticsConsent() |
Check analytics opt-in |
hasMarketingConsent() |
Check marketing opt-in |
openPreferences() |
Re-open banner panel |
syncConsentFromStorage() |
Re-apply stored consent after GTM loads |
Google Consent Mode
| Function | Description |
|---|---|
getConsentBootstrapScript() |
Inline script for vanilla HTML |
applyConsentDefaults() |
Set denied defaults programmatically |
applyConsentUpdate(consent) |
Push user choice to gtag |
buildGoogleConsentState(consent) |
Get signal object |
Events
| Constant | Fired when |
|---|---|
EVENT_CONSENT_UPDATED |
User saves a choice |
EVENT_OPEN_PREFERENCES |
Preferences panel should open |
GTM_CONSENT_EVENT |
Pushed to dataLayer (cookie_consent_update) |
CookieConsent props
| Prop | Type | Description |
|---|---|---|
labels |
object |
Override banner text |
theme |
object |
Override banner colors |
cookies |
CookieEntry[] |
Optional cookie inventory table |
policyHref |
string |
Privacy / cookie policy URL |
LinkComponent |
component |
Custom link component (default: Next.js Link) |
className |
string |
Extra CSS class on the banner root |
onConsentSaved |
(consent) => void |
Called after a successful save |
onConsentError |
({ reason }) => void |
Called when save fails |
Vanilla HTML (no React)
<head>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
ad_storage: 'denied',
analytics_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
functionality_storage: 'denied',
personalization_storage: 'denied',
security_storage: 'granted',
wait_for_update: 500
});
</script>
<!-- GTM snippet here -->
</head>
Or generate the script programmatically:
import { getConsentBootstrapScript } from "@pdntechnology/cookie-consent";
console.log(getConsentBootstrapScript());
GTM integration checklist
- Enable Consent Mode in your GTM container settings
- Configure GA4 / Google Ads tags to require appropriate consent signals
- Optional: create a trigger on custom event
cookie_consent_update - Test with Tag Assistant
Regional defaults (EEA example)
configureConsent({
regionDefaults: [
{
regions: ["AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR",
"DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL",
"PL", "PT", "RO", "SK", "SI", "ES", "SE", "IS", "LI", "NO"],
ad_storage: "denied",
analytics_storage: "denied",
ad_user_data: "denied",
ad_personalization: "denied",
},
],
});
Troubleshooting
| Problem | Likely cause | Fix |
|---|---|---|
| Banner never appears | Consent already stored | Clear localStorage key or call openPreferences() |
| GTM fires before consent | Bootstrap missing or loaded too late | Add <ConsentBootstrap /> with beforeInteractive before GTM |
| Consent not saved | Storage blocked (private mode, Safari ITP) | Handle onConsentError; show inline error from banner |
| Tags still fire after reject | GTM tags not using Consent Mode | Enable Consent Mode in GTM and set tag consent requirements |
TypeScript errors on ./react |
Missing types path | Import from @pdntechnology/cookie-consent/react (types included since v1.1) |
# In this repo
cd cookie-consent
npm link
# In your Next.js app
npm link @pdntechnology/cookie-consent
Or use a file dependency:
{
"dependencies": {
"@pdntechnology/cookie-consent": "file:../cookie-consent"
}
}
Publishing to npm
npm login
npm publish --access public
Contributing
See CONTRIBUTING.md. Bug reports and pull requests are welcome.