A user lands on your website. It takes 200 milliseconds to load the DOM, another 300 for React to hydrate, and the consent banner appears half a second later. By then, Google Analytics has already sent the page_view event, Meta Pixel has fired PageView, and Hotjar has started recording the session. The user sees the banner, thinks for a moment, and clicks “Reject All”. But their data is already on its way to servers in the United States, Ireland, and Singapore.
This scenario happens on the vast majority of websites that “comply” with GDPR. Technically they have a banner. Technically they save preferences. But tracking scripts load before the user has a chance to decide. It’s cosmetic compliance, not real compliance.
The problem multiplies when your application serves users from different countries. GDPR in Europe requires explicit opt-in before any tracking. CCPA in California allows opt-out, but you have to respect the “Do Not Sell” signal. LGPD in Brazil has its own rules. POPIA in South Africa, PDPA in Thailand, PIPL in China. There are 52 different privacy laws with requirements ranging from “you can track by default” to “you need explicit consent for each individual service”.
Implementing this correctly means weeks of work. Researching each law, implementing geo-detection, configuring each analytics service, creating a category system, handling granular consent, generating audit logs for when the data protection auditor comes. And after all that work, you discover your implementation has a bug and scripts are still loading before consent.
Why most banners don’t really comply
The fundamental problem is execution order. In a typical React application, HTML includes script tags in the <head> or at the end of <body>. These scripts start executing before React mounts any component. Your consent banner is a React component that renders after the application hydrates. By the time the banner exists in the DOM, tracking scripts have been running for hundreds of milliseconds.
Some solutions try to fix this with CSS. They add display: none to content until the user consents. This visually hides content but doesn’t prevent scripts from executing. Google Analytics still counts the visit. Conversion pixels still fire.
Other solutions use the type="text/plain" attribute on tracking scripts and change it to type="text/javascript" after consent. This works for scripts you control, but not for scripts that are dynamically injected. Google Tag Manager, for example, injects additional scripts after loading. Blocking GTM’s initial script doesn’t block the scripts GTM injects later.
The most problematic case is third-party scripts that load more scripts. Meta Pixel loads an initial script that then loads additional modules based on configuration. TikTok Pixel does the same. Hotjar loads a script that then loads the session recorder. Blocking the parent script doesn’t guarantee blocking the children if the load already triggered.
The only real solution is to intercept at the DOM level. It’s not enough to control which scripts you include in your HTML. You need to intercept any script that tries to add itself to the DOM, wherever it comes from, and block it until valid consent exists.
Why I created this library
I needed to implement cookie consent for a SaaS application with international presence. Users in Europe requiring strict GDPR with explicit opt-in. Users in California under CCPA with opt-out and “Do Not Sell” requirements. Users in Brazil with LGPD, in Canada with PIPEDA, in Asia with PDPA variations. One product, dozens of different laws, and the expectation that everything would work without the development team spending months researching privacy legislation.
Existing solutions had limitations. Paid services like Cookiebot or OneTrust cost hundreds of euros per month and add latency because they load from their servers. The open source libraries I found either just showed the banner without actually blocking scripts, or required manual configuration of each service, or didn’t support geo-detection.
What I needed was a library that actually blocked scripts at the DOM level, that automatically detected the user’s location and applied the correct law, that had the most common services pre-configured so I didn’t have to research what cookies each one uses, and that generated audit logs because data protection auditors ask for them.
The result is react-consent-shield: real script blocking with MutationObserver, 52 privacy laws with geo-detection, 274 pre-configured services from Google Analytics to Yandex Metrica, granular consent system at the individual service level, cookie scanner to detect undeclared cookies, and audit report generation with cryptographic verification.
Installation and basic usage
npm install react-consent-shield
The simplest case requires three components: the provider that manages state, the banner that shows options, and optionally the modal for detailed configuration:
import {
ConsentProvider,
ConsentBanner,
ConsentModal,
googleAnalytics,
metaPixel,
} from 'react-consent-shield';
function App() {
return (
<ConsentProvider
config={{
services: [googleAnalytics, metaPixel],
}}
>
<YourApp />
<ConsentBanner />
<ConsentModal />
</ConsentProvider>
);
}
With this minimal configuration, Google Analytics and Meta Pixel scripts are blocked until the user consents. The banner appears automatically for new users. Preferences are persisted in localStorage and respected on subsequent visits.
Services are imported as presets. Each preset includes the script patterns to block, the cookie names the service creates, the consent category it belongs to, and the information shown to the user about what data it collects.
How real script blocking works
The library uses MutationObserver to intercept DOM modifications. When any code tries to add a <script> element to the document, the observer detects it before the browser processes the script.
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeName === 'SCRIPT') {
const src = node.getAttribute('src');
if (shouldBlock(src)) {
// Convert to inert element
node.type = 'text/blocked';
node.setAttribute('data-consent-blocked', 'true');
node.setAttribute('data-original-src', src);
node.removeAttribute('src');
}
}
});
});
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
The observer activates before React mounts. It’s registered in the document’s <head> as an inline script that executes immediately. This guarantees that any tracking script that tries to load, whether in the initial HTML or dynamically injected by another script, goes through the filter.
When the user gives consent, blocked scripts are restored:
function unblockScripts(category: string) {
const blocked = document.querySelectorAll(
`script[data-consent-blocked="true"][data-category="${category}"]`
);
blocked.forEach((script) => {
const newScript = document.createElement('script');
newScript.src = script.getAttribute('data-original-src');
newScript.type = 'text/javascript';
script.parentNode.replaceChild(newScript, script);
});
}
This approach has an important advantage: it works with any script, not just ones you control. If a third-party script tries to inject another tracking script, it also gets blocked. If a tag manager tries to load additional pixels, they also get blocked. Blocking happens at the DOM level, not at the configuration level.
Automatic geographic detection
Manually implementing the logic of “if user is in Germany apply GDPR, if in California apply CCPA, if in Brazil apply LGPD” is tedious and error-prone. The library includes geo-detection that automatically determines which law applies.
<ConsentProvider
config={{
services: [googleAnalytics, metaPixel],
geoDetection: {
enabled: true,
fallbackLaw: 'GDPR',
},
}}
>
Detection uses the browser’s geolocation API when available, with fallback to timezone and browser language detection. The result determines which privacy law applies:
| Region | Law | Behavior |
|---|---|---|
| EU (27 countries) | GDPR | Mandatory opt-in, explicit consent |
| United Kingdom | UK-GDPR | Mandatory opt-in, post-Brexit |
| California | CCPA | Opt-out allowed, “Do Not Sell” |
| Virginia, Colorado, Connecticut | US state laws | Opt-out variations |
| Brazil | LGPD | Opt-in for sensitive data |
| Canada | PIPEDA | Implicit consent allowed in some cases |
| South Africa | POPIA | Opt-in with exceptions |
| Singapore, Thailand | PDPA | Regional variations |
| China | PIPL | Strict localization requirements |
| 40+ others | Local laws | Jurisdiction-specific configuration |
The fallbackLaw determines which rules to apply when detection fails or the user is in a region without a specific privacy law. Setting GDPR as fallback means applying the strictest rules by default, which guarantees compliance in the worst case.
Each law has different configuration for cookie categories. GDPR requires consent for analytics and marketing. CCPA allows analytics by default but requires an opt-out option for data “sale”. Some Asian laws allow functional cookies without consent but require opt-in for personalization.
The 274 pre-configured services
Researching what cookies Google Analytics creates, what domains Meta Pixel uses, what scripts Hotjar loads, consumes hours. The library includes 274 pre-configured services with all this information:
import {
// Analytics
googleAnalytics,
googleTagManager,
metaPixel,
tiktokPixel,
hotjar,
mixpanel,
amplitude,
segment,
heap,
fullstory,
microsoftClarity,
// Advertising
googleAds,
metaAds,
linkedInAds,
twitterAds,
pinterestTag,
snapchatPixel,
criteo,
// Regional
yandexMetrica, // Russia
baiduAnalytics, // China
naverAnalytics, // South Korea
// Privacy-focused
plausible,
fathom,
simpleAnalytics,
} from 'react-consent-shield';
Each preset is an object with the service’s complete configuration:
const googleAnalytics: ServicePreset = {
id: 'google-analytics',
name: 'Google Analytics',
category: 'analytics',
description: 'Traffic analysis and user behavior',
privacyPolicy: 'https://policies.google.com/privacy',
scripts: [
/googletagmanager\.com\/gtag/,
/google-analytics\.com\/analytics/,
/googleanalytics\.com/,
],
cookies: [
{ name: '_ga', duration: '2 years', description: 'Distinguishes users' },
{ name: '_gid', duration: '24 hours', description: 'Distinguishes users' },
{ name: '_gat', duration: '1 minute', description: 'Throttles request rate' },
],
dataCollected: [
'Pages visited',
'Time on page',
'Traffic source',
'Device and browser',
'Approximate location',
],
};
The scripts patterns are regular expressions that match URLs of scripts to block. The library intercepts any script whose URL matches any of these patterns.
The cookies list the cookies the service creates. This information is shown to the user in the detailed configuration modal and used by the cookie scanner that detects undeclared cookies.
For services not in the list, you can create custom presets:
const myCustomService: ServicePreset = {
id: 'my-analytics',
name: 'My Custom Analytics',
category: 'analytics',
scripts: [/my-analytics\.com\/track/],
cookies: [
{ name: 'ma_id', duration: '1 year', description: 'User ID' },
],
};
<ConsentProvider
config={{
services: [googleAnalytics, myCustomService],
}}
>
Google Consent Mode v2
Google introduced Consent Mode as a way to communicate consent state to their services. Version 2, mandatory since March 2024 for sites using Google Ads in the EEA, adds additional signals for personalized advertising.
The library integrates Consent Mode automatically:
<ConsentProvider
config={{
services: [googleAnalytics, googleAds],
googleConsentMode: {
enabled: true,
defaultState: {
analytics_storage: 'denied',
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
functionality_storage: 'granted',
personalization_storage: 'denied',
security_storage: 'granted',
},
},
}}
>
When the user modifies their consent, the library automatically updates Consent Mode signals:
// User accepts analytics but rejects ads
gtag('consent', 'update', {
analytics_storage: 'granted',
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
});
Google Analytics in denied mode still sends pings but without cookies or persistent identifiers. This allows getting aggregate traffic data while respecting the user’s decision. Google Ads with ad_storage: denied can’t do remarketing but can still attribute conversions in aggregate.
The useGoogleConsentMode hook allows accessing the current state of signals:
function ConsentDebugger() {
const { consentState, updateConsent } = useGoogleConsentMode();
return (
<div>
<p>Analytics: {consentState.analytics_storage}</p>
<p>Ads: {consentState.ad_storage}</p>
<p>Ad User Data: {consentState.ad_user_data}</p>
<p>Ad Personalization: {consentState.ad_personalization}</p>
</div>
);
}
Total granular consent system
Most banners offer three buttons: “Accept All”, “Reject All”, and “Customize”. Configuration is usually by broad categories: analytics, marketing, functional. This doesn’t give real control to the user who wants to allow Google Analytics but not Meta Pixel, or who wants Hotjar for feedback but doesn’t want their sessions recorded.
The library supports three levels of granularity:
Level 1: All or nothing
<ConsentProvider
config={{
services: [googleAnalytics, metaPixel, hotjar],
granularConsent: false,
}}
>
The user accepts or rejects everything. Simple but inflexible.
Level 2: By categories
<ConsentProvider
config={{
services: [googleAnalytics, metaPixel, hotjar],
granularConsent: true,
categories: {
analytics: { required: false, defaultEnabled: false },
marketing: { required: false, defaultEnabled: false },
functional: { required: true, defaultEnabled: true },
},
}}
>
The user can accept analytics but reject marketing. Services are automatically grouped by their category in the preset.
Level 3: By individual service
<ConsentProvider
config={{
services: [googleAnalytics, metaPixel, hotjar, mixpanel, amplitude],
granularConsent: true,
serviceLevel: true,
}}
>
The user sees each service individually and can decide: “I want Google Analytics but not Mixpanel. I want Hotjar but only the feedback widget, not session recording.”
The detailed configuration modal shows each service with its description, what data it collects, what cookies it creates, and a link to its privacy policy:
<ConsentModal
showServiceDetails={true}
showCookieInfo={true}
showDataCollected={true}
showPrivacyLinks={true}
/>
For implementations that need programmatic control, hooks expose the complete state:
function MyCustomUI() {
const {
services,
categories,
isServiceEnabled,
toggleService,
toggleCategory,
enableAll,
disableAll,
} = useConsent();
return (
<div>
{services.map(service => (
<label key={service.id}>
<input
type="checkbox"
checked={isServiceEnabled(service.id)}
onChange={() => toggleService(service.id)}
/>
{service.name}
</label>
))}
</div>
);
}
Cookie Scanner: detecting undeclared cookies
The undeclared cookies problem is common. You install a WordPress plugin and it creates cookies you didn’t know about. A third-party script injects additional tracking. A developer adds localStorage for something and nobody documents it.
The cookie scanner detects cookies that exist in the browser but aren’t declared in any configured service:
import { useCookieScanner } from 'react-consent-shield';
function CookieAudit() {
const { scan, results, isScanning } = useCookieScanner();
useEffect(() => {
scan();
}, []);
if (isScanning) return <p>Scanning cookies...</p>;
return (
<div>
<h3>Declared cookies ({results.declared.length})</h3>
<ul>
{results.declared.map(cookie => (
<li key={cookie.name}>
{cookie.name} - {cookie.service}
</li>
))}
</ul>
<h3>Undeclared cookies ({results.undeclared.length})</h3>
<ul>
{results.undeclared.map(cookie => (
<li key={cookie.name} style={{ color: 'red' }}>
{cookie.name} - UNKNOWN ORIGIN
</li>
))}
</ul>
</div>
);
}
The scanner compares current browser cookies against the list of cookies declared in all configured services. Cookies that don’t match any service appear as “undeclared”.
This is critical for compliance audits. A data protection auditor may request the list of all cookies your site creates. If there are cookies not documented in your privacy policy, you have a problem. The scanner lets you detect this before the auditor does.
The scanner also detects third-party cookies created without your knowledge:
const { results } = useCookieScanner({
includeThirdParty: true,
includeSessionStorage: true,
includeLocalStorage: true,
});
// results.thirdParty contains cookies from other domains
// results.localStorage contains localStorage keys
// results.sessionStorage contains sessionStorage keys
Audit logging and reports for audits
When a data protection auditor visits, they need to see evidence that your consent system works correctly. It’s not enough to show them the banner. They want to see records of when each user gave consent, what options they chose, if they changed their mind, and that these records haven’t been tampered with.
The library generates audit logs with cryptographic verification:
<ConsentProvider
config={{
services: [googleAnalytics, metaPixel],
auditLog: {
enabled: true,
hashAlgorithm: 'SHA-256',
includeUserAgent: true,
includeTimestamp: true,
storage: 'localStorage', // or 'server' to send to your backend
},
}}
>
Each consent action generates a record:
{
id: 'consent-log-1701432000000',
timestamp: '2025-12-01T12:00:00.000Z',
action: 'consent-updated',
previousState: {
analytics: false,
marketing: false,
},
newState: {
analytics: true,
marketing: false,
},
services: {
'google-analytics': true,
'meta-pixel': false,
'hotjar': true,
},
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...',
geoLocation: 'DE',
lawApplied: 'GDPR',
hash: 'a1b2c3d4e5f6...',
previousHash: '9z8y7x6w5v...',
}
The hash field is a SHA-256 of the record content plus the previous record’s hash. This creates a chain where modifying any record invalidates all subsequent ones. If the auditor sees that hashes don’t match, they know someone tampered with the logs.
To access logs programmatically:
import { useAuditLog } from 'react-consent-shield';
function AuditViewer() {
const { logs, exportLogs, verifyIntegrity } = useAuditLog();
const handleExport = () => {
const data = exportLogs({ format: 'json' });
downloadFile(data, 'consent-audit-log.json');
};
const handleVerify = async () => {
const isValid = await verifyIntegrity();
alert(isValid ? 'Logs intact' : 'ALERT: Logs tampered');
};
return (
<div>
<button onClick={handleExport}>Export logs</button>
<button onClick={handleVerify}>Verify integrity</button>
<table>
<thead>
<tr>
<th>Date</th>
<th>Action</th>
<th>Services</th>
<th>Hash</th>
</tr>
</thead>
<tbody>
{logs.map(log => (
<tr key={log.id}>
<td>{new Date(log.timestamp).toLocaleString()}</td>
<td>{log.action}</td>
<td>{Object.keys(log.services).filter(s => log.services[s]).join(', ')}</td>
<td>{log.hash.substring(0, 16)}...</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
The library also generates compliance reports in JSON and HTML formats:
import { generateComplianceReport } from 'react-consent-shield';
const report = generateComplianceReport({
format: 'html', // or 'json'
includeServiceDetails: true,
includeCookieInventory: true,
includeAuditSummary: true,
includeLawConfiguration: true,
});
// The report includes:
// - List of all configured services
// - Complete cookie inventory
// - Audit log summary
// - Configuration for each privacy law
// - Blocked and unblocked scripts
// - Cookie scanner results
The HTML report is formatted for printing and delivering to auditors. It includes tables with all services, their cookies, what data they collect, and the current consent state.
Additional features
Age verification
For sites accessible to minors, COPPA (USA) and GDPR-K (GDPR Article 8) require age verification:
<ConsentProvider
config={{
services: [googleAnalytics],
ageVerification: {
enabled: true,
method: 'birthdate', // 'checkbox' | 'year' | 'birthdate'
minimumAge: 16,
parentalConsentRequired: true,
blockTrackingIfUnderage: true,
},
}}
>
If the user indicates they’re under the minimum age, all tracking scripts remain blocked regardless of other options.
Sharing consent across subdomains
For applications with multiple subdomains (app.example.com, blog.example.com, shop.example.com), consent given on one should apply to all:
<ConsentProvider
config={{
services: [googleAnalytics],
storage: {
type: 'cookie',
domain: '.example.com', // Note the leading dot
sameSite: 'lax',
},
}}
>
The consent cookie is created with domain .example.com, making it accessible from any subdomain.
Consent versioning
When you add a new service or change configuration, you may need to request consent again:
<ConsentProvider
config={{
services: [googleAnalytics, metaPixel, newService],
consentVersion: '2.0',
onVersionChange: (oldVersion, newVersion) => {
// Optionally show banner again
return true; // true = request consent again
},
}}
>
The library detects changes in the service list and can automatically invalidate previous consent, showing the banner even if the user had already chosen before.
Banner customization
The library includes 8 pre-designed banner variants:
<ConsentBanner
variant="default" // Standard bottom bar
// variant="fullwidth" // Full-width bar
// variant="modal" // Centered modal
// variant="floating" // Floating with shadow
// variant="card" // Compact card
// variant="minimal" // Text and buttons only
// variant="corner" // Bottom corner
// variant="sidebar" // Side panel
position="bottom" // 'top' | 'bottom' | 'bottom-left' | 'bottom-right'
theme="light" // 'light' | 'dark' | 'auto'
/>
All texts are customizable:
<ConsentBanner
texts={{
title: 'We use cookies',
description: 'This site uses cookies to improve your experience.',
acceptAll: 'Accept all',
rejectAll: 'Reject all',
customize: 'Customize',
privacyPolicy: 'Privacy policy',
}}
/>
The library includes translations for 10 languages:
<ConsentProvider
config={{
services: [googleAnalytics],
language: 'en', // 'en' | 'es' | 'de' | 'fr' | 'pt' | 'it' | 'nl' | 'pl' | 'ja' | 'zh'
}}
>
For languages not included, you can provide custom translations:
<ConsentProvider
config={{
services: [googleAnalytics],
translations: {
title: 'Käytämme evästeitä',
description: 'Tämä sivusto käyttää evästeitä...',
acceptAll: 'Hyväksy kaikki',
rejectAll: 'Hylkää kaikki',
// ... rest of texts
},
}}
>
Accessibility is full WCAG 2.2 AA: keyboard navigation, screen reader support, focus trap in modals, sufficient contrast in all themes.
Real use cases
E-commerce with multiple regions
<ConsentProvider
config={{
services: [
googleAnalytics,
googleAds,
metaPixel,
criteo,
hotjar,
],
geoDetection: { enabled: true, fallbackLaw: 'GDPR' },
googleConsentMode: { enabled: true },
granularConsent: true,
auditLog: { enabled: true },
}}
>
The online store automatically detects if the user is in Europe (strict GDPR), California (CCPA with opt-out), or rest of world. Google Consent Mode allows continuing to measure aggregate conversions even when users reject cookies.
B2B SaaS with compliance requirements
<ConsentProvider
config={{
services: [
mixpanel,
amplitude,
segment,
intercom,
fullstory,
],
granularConsent: true,
serviceLevel: true,
auditLog: {
enabled: true,
storage: 'server',
endpoint: '/api/consent-logs',
},
complianceReport: { enabled: true },
}}
>
The SaaS product allows granular control per service because its enterprise customers demand it. Audit logs are sent to the server for centralized storage. Compliance reports are generated to include in security documentation.
Personal blog with respectful analytics
<ConsentProvider
config={{
services: [plausible],
banner: {
variant: 'minimal',
position: 'bottom-right',
},
}}
>
Plausible doesn’t use cookies and may not require consent in some jurisdictions, but showing a banner demonstrates transparency and builds trust with readers.
Testing and quality
The library has 323 tests with 100% coverage across all metrics:
| Metric | Coverage |
|---|---|
| Statements | 100% |
| Branches | 100% |
| Functions | 100% |
| Lines | 100% |
Tests cover:
- Script blocking for each of the 274 services
- Geographic detection for all 52 supported laws
- Complete translations in all 10 languages
- Google Consent Mode integration
- Cookie scanner with different browser configurations
- Audit log generation and verification
- Accessibility with simulated screen readers
- Rendering of all banner variants
Verified compatibility:
- React 18 and React 19
- Next.js 14 and Next.js 15
- TypeScript 5.3+
- Node.js 18+
- Chrome 80+, Firefox 75+, Safari 13+, Edge 80+
The package is published on NPM as react-consent-shield. Source code is at github.com/686f6c61/react-consent-shield. The interactive demo is at react-consent-shield.onrender.com where you can test all configurations in real-time.