@meui-creative/cookies
v7.0.3
Published
GDPR-compliant cookie consent management for React with script blocking and Google Consent Mode V2
Downloads
668
Readme
@meui-creative/cookies
GDPR-compliant cookie consent for React. Script blocking, Google Consent Mode V2, 5 preset layouts, dark mode, 9 languages, GPC support, WCAG 2.1 AA accessible.
Installation
npm install @meui-creative/cookies framer-motion lucide-reactframer-motion and lucide-react are only needed for the built-in UI presets. For headless usage (hook only), install just the main package.
How It Works
- You define your tracking scripts with
type="text/plain"anddata-cookie-category="..."— this prevents them from executing - The library shows a consent banner to the user
- When the user accepts, scripts matching consented categories are unblocked and executed
- Google Consent Mode V2 signals are updated automatically
Quick Start (Next.js App Router)
1. Define your scripts in the layout
Mark each script with type="text/plain" and the appropriate data-cookie-category. The library will unblock them after consent.
// app/layout.tsx
import Script from 'next/script'
import { CookieConsent } from '@meui-creative/cookies'
import '@meui-creative/cookies/styles'
const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID
const GA_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID
const PIXEL_ID = process.env.NEXT_PUBLIC_FACEBOOK_PIXEL_ID
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="cs">
<body>
{/* Google Tag Manager — blocked until "analytics" consent */}
{GTM_ID && (
<Script
id="gtm"
type="text/plain"
data-cookie-category="analytics"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','${GTM_ID}');
`,
}}
/>
)}
{/* Google Analytics 4 — blocked until "analytics" consent */}
{GA_ID && (
<>
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`}
type="text/plain"
data-cookie-category="analytics"
strategy="afterInteractive"
/>
<Script
id="ga4"
type="text/plain"
data-cookie-category="analytics"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GA_ID}');
`,
}}
/>
</>
)}
{/* Facebook Pixel — blocked until "marketing" consent */}
{PIXEL_ID && (
<Script
id="facebook-pixel"
type="text/plain"
data-cookie-category="marketing"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '${PIXEL_ID}');
fbq('track', 'PageView');
`,
}}
/>
)}
{/* Cookie consent banner */}
<CookieConsent preset="meui" language="cs" accentColor="#C0B7A7" />
{children}
</body>
</html>
)
}2. That's it
No integrations prop, no auto-injection. You control your scripts, the library controls consent.
Script & Iframe Blocking
Any element with type="text/plain" and data-cookie-category is blocked until the user consents to that category:
<!-- External script — blocked until "analytics" consent -->
<script type="text/plain" data-cookie-category="analytics" src="https://example.com/analytics.js"></script>
<!-- Inline script -->
<script type="text/plain" data-cookie-category="marketing">
console.log('Runs only after marketing consent');
</script>
<!-- Iframes — src is stripped until consent -->
<iframe data-cookie-category="marketing" src="https://www.youtube.com/embed/VIDEO_ID"></iframe>A MutationObserver handles dynamically added elements too.
Categories: strictly-necessary (always on), functional, analytics, marketing
Entry Points
| Import | What you get |
|--------|-------------|
| @meui-creative/cookies | CookieConsent component + useCookieConsent hook + API |
| @meui-creative/cookies/headless | Hook + API only (zero UI dependencies) |
| @meui-creative/cookies/presets | Individual preset components |
| @meui-creative/cookies/styles | CSS stylesheet |
Design Presets
default — Classic Card
Blue (#3b82f6), bottom-left. Clean card with cookie icon.
<CookieConsent />meui — Compact
Green (#4A6953), bottom-left. Pill-shaped buttons, inline expandable settings.
<CookieConsent preset="meui" />minimal — Bottom Bar
Black (#000), full-width bottom bar.
<CookieConsent preset="minimal" />card — Accent Card
Violet (#8b5cf6), bottom-right. Stacked buttons.
<CookieConsent preset="card" />sidebar — Full-Height Panel
Emerald (#10b981), right sidebar slide-in with trust indicator.
<CookieConsent preset="sidebar" />Customization
<CookieConsent
preset="default"
accentColor="#ec4899"
borderRadius="sharp" // 'rounded' | 'sharp'
language="cs" // 'en' | 'cs' | 'de'
descriptionMode="long" // 'short' | 'long'
mode="strict" // 'strict' (closing = decline) | 'soft'
showSettingsButton={true}
closeButton={true}
showRejectLink={true}
version="1.0.0" // Bump to force re-consent
ttl={365} // Consent expiry in days
domain=".example.com" // Cookie domain for subdomains
style={{
position: 'top-right', // 'bottom-left' | 'bottom-center' | 'bottom-right' | 'top-left' | 'top-center' | 'top-right'
maxWidth: 'lg', // 'sm' | 'md' | 'lg' | 'xl'
spacing: 'comfortable', // 'compact' | 'normal' | 'comfortable'
background: '#ffffff',
shadow: '2xl', // 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
border: { width: '2', color: '#333' },
fontSize: { heading: 'lg', body: 'sm' },
button: { shape: 'pill', size: 'lg' },
animation: { direction: 'up', duration: 0.3, enabled: true },
}}
links={{ privacyPolicy: '/privacy', cookiePolicy: '/cookies' }}
onAccept={(choices) => console.log('Accepted:', choices)}
onDecline={() => console.log('Declined')}
onChange={(choices) => console.log('Changed:', choices)}
onReady={() => console.log('Loaded from storage')}
onBlock={(src) => console.log('Script unblocked:', src)}
onVersionChange={(prev) => console.log('Version changed from:', prev)}
debug={false}
/>Dark Mode
// Explicit dark mode
<CookieConsent theme="dark" />
// Follow system preference (prefers-color-scheme)
<CookieConsent theme="auto" />
// Default: light
<CookieConsent theme="light" />All presets fully support dark mode. Colors adapt automatically based on the theme.
Global Privacy Control (GPC)
The library automatically detects the navigator.globalPrivacyControl signal. When detected, it auto-declines all non-essential cookies without showing a banner.
// Enabled by default
<CookieConsent respectGPC={true} />
// Disable if needed
<CookieConsent respectGPC={false} />GPC is legally required in California (CCPA), Colorado, and Connecticut.
Cookie Auto-Clear
When a user rejects a category, automatically delete cookies set by that category:
<CookieConsent
customCategories={[
{
id: 'analytics',
label: 'Analytics',
description: 'Help us improve',
required: false,
autoClear: {
cookies: [
{ name: '_ga' },
{ name: '_gid' },
{ name: '_ga_XXXXXXX', domain: '.example.com' },
],
},
},
]}
/>Consent Record Logging
For GDPR Article 7(1) proof-of-consent, send consent records to your server:
<CookieConsent
onConsentRecord={(record) => {
// record: { version, timestamp, choices, userAgent, language, consentMethod, sourceUrl, gpcSignal }
fetch('/api/consent-log', {
method: 'POST',
body: JSON.stringify(record),
})
}}
/>Languages
Built-in: English, Czech, German, French, Spanish, Italian, Polish, Dutch, Slovak.
<CookieConsent language="fr" />Custom translations override any built-in text:
<CookieConsent
language="en"
customTexts={{
heading: 'Cookie Notice',
acceptButton: 'I agree',
declineButton: 'No thanks',
}}
/>Accessibility (WCAG 2.1 AA)
role="dialog"+aria-modalon settings modalaria-labelledby+aria-describedbylinking heading and descriptionrole="region"+aria-labelon banner- Full focus trap in modal (Tab/Shift+Tab cycle, no escape)
- Focus restoration on modal close
- Scroll lock when modal is open
role="checkbox"+aria-checkedon category toggles- Keyboard navigation (Enter/Space to toggle, Escape to close)
Google Consent Mode V2
The hook/component pushes a consent: 'default' (all denied) on init and a consent: 'update' on every choice, via the canonical gtag(){ dataLayer.push(arguments) } form so GTM/gtag.js actually registers it. The update sets every signal explicitly (granted/denied), so a decline or a preference change genuinely denies — it never leaves a revoked category stuck as granted.
Requires ≥ 7.0.2. Earlier versions pushed commands as a plain array (ignored by GTM) and/or pushed a partial update (revoked categories not denied).
Category → signal mapping:
| Category | Consent Mode signals granted on opt-in |
|----------|----------------------------------------|
| strictly-necessary | functionality_storage |
| functional | functionality_storage, personalization_storage |
| analytics | analytics_storage |
| marketing | ad_storage, ad_user_data, ad_personalization |
| (always) | security_storage |
Two integration modes — pick one per tag
Basic — block the tag until consent. Mark the GTM/GA/Pixel script with type="text/plain" + data-cookie-category. The library unblocks it on consent. Nothing else to do — the hook's default/update run fine because the tag is blocked anyway.
Advanced — GTM loads on every visit (cookieless pings). Drop the type="text/plain" so GTM always loads, and gate individual tags inside GTM via Consent Mode. ⚠️ In this mode you must set the consent: 'default' synchronously in <head>, before GTM — the hook sets its default in a useEffect (after hydration), which races GTM. Then set window.__meui_consent_mode_initialized__ = true in that same head script so the hook does not push a second, late default. The hook still pushes the update on the user's choice.
// app/layout.tsx — inside <head>, BEFORE the GTM snippet.
// Region-scoped (Google best practice): granted worldwide, denied for EEA/UK/CH.
<script
id="consent-mode-default"
dangerouslySetInnerHTML={{
__html:
`window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}` +
`gtag('consent','default',{ad_storage:'granted',ad_user_data:'granted',` +
`ad_personalization:'granted',analytics_storage:'granted',` +
`functionality_storage:'granted',personalization_storage:'granted',security_storage:'granted'});` +
`gtag('consent','default',{ad_storage:'denied',ad_user_data:'denied',` +
`ad_personalization:'denied',analytics_storage:'denied',functionality_storage:'denied',` +
`personalization_storage:'denied',security_storage:'granted',wait_for_update:500,` +
`region:['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','GB','CH']});` +
`gtag('set','url_passthrough',true);gtag('set','ads_data_redaction',true);` +
`window.__meui_consent_mode_initialized__=true;`,
}}
/>A single worldwide denied default is also valid (simpler, just drop the first granted default and the region array) — region scoping only preserves measurement where consent isn't legally required.
Optional configuration (component / basic mode)
For the bundled CookieConsent component or basic mode, the same options are available on the hook config (region, urlPassthrough, adsDataRedaction):
<CookieConsent
consentModeOptions={{
region: ['EU', 'US-CA'],
urlPassthrough: true,
adsDataRedaction: true,
}}
/>GPC + headless: the bundled
CookieConsentcomponent honours Global Privacy Control (respectGPC). The headless hook does not — wire it yourself (see Headless Mode below).
Headless Mode
Use the hook only, build your own UI:
import { useCookieConsent } from '@meui-creative/cookies/headless'
function MyBanner() {
const {
consent, // { 'strictly-necessary': true, analytics: false, ... }
hasConsent, // Has user made any choice?
isReady, // Initialization complete?
categories, // CategoryDefinition[]
acceptAll,
declineAll,
setConsent, // Granular: setConsent({ analytics: true, marketing: false })
revokeConsent,
exportConsent,
showSettings,
} = useCookieConsent({ version: '1.0.0', language: 'en' })
if (!isReady || hasConsent) return null
return (
<div>
<p>We use cookies</p>
<button onClick={acceptAll}>Accept All</button>
<button onClick={declineAll}>Decline</button>
</div>
)
}The headless hook does not auto-handle GPC (only the bundled CookieConsent component does). If you want it in a custom banner, add it yourself:
// Auto-decline non-essential when the browser sends Global Privacy Control and
// the visitor hasn't chosen yet (legally binding in CA/CO/CT, USA).
useEffect(() => {
if (!isReady || hasConsent) return
const nav = navigator as Navigator & { globalPrivacyControl?: boolean }
if (nav.globalPrivacyControl === true) declineAll()
}, [isReady, hasConsent, declineAll])Re-opening settings from a footer link: showCookieSettings() (or the hook's showSettings) dispatches the meui:settings:opened window event — a custom banner must listen for it to re-open.
Programmatic API
import { showCookieSettings, revokeConsent, exportConsent } from '@meui-creative/cookies'
// Open settings modal from anywhere (e.g. footer link)
showCookieSettings()
// Revoke all consent
revokeConsent()
// Export consent record for GDPR data subject requests
const record = exportConsent()
// { version, timestamp, choices, userAgent, language, consentMethod, sourceUrl }Events
window.addEventListener('meui:consent:given', (e) => {
console.log('Consent given:', e.detail.consent)
})
window.addEventListener('meui:consent:revoked', () => {
console.log('Consent revoked')
})
window.addEventListener('meui:consent:version-changed', (e) => {
console.log('Re-consent needed, previous version:', e.detail.previousVersion)
})Storage
- localStorage (
meui_cookie_consent) — Full audit record: version, timestamp, choices, userAgent, language, consentMethod, sourceUrl - Cookie (
meui_cookie_consent) — Comma-separated list of consented categories (for server-side reading)
License
MIT
