freegdpr
v1.0.0
Published
Standalone, zero-dependency GDPR cookie consent banner: script-blocking, Google Consent Mode v2, accessible Shadow DOM UI. Usable via CDN or npm.
Downloads
145
Maintainers
Readme
FreeGDPR.js
Standalone, zero-dependency GDPR cookie-consent banner — script-blocking, Google Consent Mode v2, accessible Shadow-DOM UI. Drop in via CDN or npm.
FreeGDPR.js makes any website GDPR/ePrivacy compliant for cookie consent. It shows a consent banner, a granular preferences panel, persists choices, blocks third-party tags until consent is given, and wires up Google Consent Mode v2 automatically — all from a single <script> tag.
Why FreeGDPR.js
Most "cookie banners" only record a choice but still let trackers fire before consent — which is exactly what regulators fine. FreeGDPR.js was designed around the parts that actually matter for compliance:
- ✅ Prior blocking — tagged
<script>/<iframe>do not execute until the matching category is granted. - ✅ Equal prominence — "Reject all" is as visible as "Accept all" on the first layer (EDPB guidance, no dark patterns).
- ✅ No pre-ticked boxes — every non-essential category defaults to off.
- ✅ Easy withdrawal — a floating button +
GDPR.openPreferences()reopen the panel any time. - ✅ Consent expiry — re-asks after a configurable period (default 6 months).
- ✅ Google Consent Mode v2 — pushes
default: deniedbefore tags load andupdateon every change. - ✅ Accessible — Shadow-DOM isolation, ARIA dialog, focus trap, keyboard +
Esc, WCAG-AA contrast.
Installation
CDN (recommended)
<!-- 1) Configure FIRST -->
<script>
window.GDPRConfig = {
companyName: 'Acme Inc.',
privacyPolicyUrl: '/privacy',
cookiePolicyUrl: '/cookie-policy',
consentMode: { enabled: true },
};
</script>
<!-- 2) Load the library (auto-initialises from GDPRConfig) -->
<script src="https://cdn.jsdelivr.net/npm/freegdpr@1/dist/freegdpr.iife.min.js"></script>Pin a version for production ([email protected]) and add SRI for integrity.
npm
npm install freegdprimport { init } from 'freegdpr';
const gdpr = init({
companyName: 'Acme Inc.',
consentMode: { enabled: true },
});Blocking third-party tags
Mark any tag you must not run before consent with type="text/plain" and data-gdpr="<category>":
<!-- Google Analytics — runs only after "analytics" consent -->
<script type="text/plain" data-gdpr="analytics"
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<!-- Inline Meta Pixel — runs only after "marketing" consent -->
<script type="text/plain" data-gdpr="marketing">
!function(f,b,e,v,n,t,s){ /* fbq bootstrap */ }();
fbq('init', 'PIXEL_ID'); fbq('track', 'PageView');
</script>
<!-- A YouTube embed — loads only after "marketing" consent -->
<iframe data-gdpr="marketing"
data-gdpr-src="https://www.youtube.com/embed/VIDEO_ID"></iframe>When the category is granted, FreeGDPR.js swaps the inert placeholder for a live, executing element. Newly granted categories activate incrementally without a reload.
Google Consent Mode v2
Enable it and FreeGDPR.js handles the rest:
window.GDPRConfig = {
consentMode: {
enabled: true,
urlPassthrough: true, // pass ad-click info without cookies
adsDataRedaction: true, // redact ads data while denied (default)
waitForUpdate: 500, // ms Google holds tags before consent
},
};Category → signal mapping is configurable per category via consentModeSignals. Defaults:
| Category | Google signals |
| ------------- | ---------------------------------------------------------- |
| necessary | security_storage |
| analytics | analytics_storage |
| marketing | ad_storage, ad_user_data, ad_personalization |
| preferences | functionality_storage, personalization_storage |
Configuration — window.GDPRConfig
All properties are optional.
| Property | Type | Default | Description |
| ----------------------- | --------- | ------------------ | ----------- |
| companyName | string | '' | Interpolated into the banner body. |
| privacyPolicyUrl | string | '#' | Privacy Policy URL (new tab). |
| cookiePolicyUrl | string | '#' | Cookie Policy URL (new tab). |
| position | string | 'bottom' | bottom · bottom-left · bottom-right · top · center. |
| overlay | boolean | false | Dim + blur the page until the user accepts/rejects. |
| theme | string | 'auto' | light · dark · auto. |
| colors | object | — | Brand colours: text, background, primary, primaryText. |
| locale | string | auto | it · en. Auto-detected (EN default, IT for Italian browsers). |
| messages | object | {} | Override any UI string by key. |
| categories | object | 4 built-ins | Merged over defaults. |
| storage | string | 'localStorage' | localStorage · cookie. |
| storageKey | string | 'gdpr_consent_v1'| Key / cookie name. |
| cookieDomain | string | — | e.g. .example.com for cross-subdomain (cookie mode). |
| consentExpiryDays | number | 180 | Re-ask after N days. 0 disables expiry. |
| autoBlock | boolean | true | Auto-activate tagged tags. |
| consentMode | object | { enabled:false }| Google Consent Mode v2. |
| showPreferencesButton | boolean | true | Floating reopen button. |
| preferencesButtonPosition | string | 'bottom-left' | Floating button side: bottom-left · bottom-right. |
| preferencesButtonOffset | number | 18 | Floating button distance (px) from the bottom edge. |
| autoShow | boolean | true | Render banner on init. |
| onAcceptAll | function | — | Fired on "Accept all". |
| onRejectAll | function | — | Fired on "Reject all". |
| onSave | function | — | Fired on every save. |
| onInit | function | — | Fired once with the existing record (or null). |
Categories
window.GDPRConfig = {
categories: {
analytics: { label: 'Analytics', description: '…', required: false, default: false },
functional: { // add your own
label: 'Functional',
description: 'Chat widget, video embeds…',
consentModeSignals: ['functionality_storage'],
},
},
};User categories are merged over the four built-ins (necessary, analytics, marketing, preferences).
Branding (name & colours)
Company / site name — set
companyName; it is injected into the banner text. To replace the whole banner copy instead, overridemessages['banner.body']. Set justcompanyNameand the default wording adapts around it.Colours —
colorslets you match your brand without touching CSS:window.GDPRConfig = { companyName: 'My Shop', colors: { text: '#1a1a2e', // titles + body text background: '#ffffff', // banner / modal background primary: '#e2007a', // buttons, links, active toggles // primaryText: '#fff' // optional — auto-derived from `primary` for contrast }, };Accepts any CSS colour (hex,
rgb(),hsl(), named). Colours apply over the chosenthemein both light and dark mode; the button text colour is auto-contrasted fromprimarywhen you don't setprimaryText.
Full HTML example (all options)
Copy-paste this into your page and trim what you don't need. Every option below is optional — the library works with an empty {}.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- 1) CONFIGURE — must come BEFORE the library script -->
<script>
window.GDPRConfig = {
// --- General ---
companyName: 'Acme Inc.', // shown in the banner body
privacyPolicyUrl: '/privacy', // opened in a new tab
cookiePolicyUrl: '/cookie-policy', // opened in a new tab
position: 'bottom-right', // bottom | bottom-left | bottom-right | top | center
overlay: false, // true = dim + blur the page until accept/reject
theme: 'auto', // light | dark | auto (prefers-color-scheme)
colors: { // brand colours (any CSS colour value)
text: '#1a1a2e', // titles + body text
background: '#ffffff', // banner / modal background
primary: '#1b56d3', // buttons, links, active toggles
// primaryText: '#ffffff', // optional; auto-derived from `primary`
},
locale: 'en', // it | en — omit to auto-detect from the browser
// --- Persistence ---
storage: 'localStorage', // localStorage | cookie
storageKey: 'gdpr_consent_v1', // localStorage key / cookie name
cookieDomain: '.example.com', // only for storage:'cookie' (cross-subdomain)
consentExpiryDays: 180, // re-ask after N days; 0 = never expire
// --- Behaviour ---
autoBlock: true, // activate tagged <script>/<iframe> on consent
autoShow: true, // show the banner automatically on first visit
showPreferencesButton: true, // floating button to reopen preferences
preferencesButtonPosition: 'bottom-left', // bottom-left | bottom-right
preferencesButtonOffset: 18, // px from the bottom edge (e.g. clear other widgets)
// --- Google Consent Mode v2 ---
consentMode: {
enabled: true,
urlPassthrough: true, // pass ad-click info without cookies
adsDataRedaction: true, // redact ads data while denied
waitForUpdate: 500, // ms Google holds tags before consent
region: [], // e.g. ['IT','FR'] to scope the defaults
defaults: {}, // force specific signals, e.g. { security_storage: 'granted' }
},
// --- Override any UI string (optional) ---
messages: {
'banner.acceptAll': 'Got it',
'banner.rejectAll': 'No thanks',
},
// --- Categories (merged over the 4 built-ins) ---
categories: {
analytics: {
label: 'Analytics',
description: 'Anonymous usage statistics.',
required: false,
default: false,
consentModeSignals: ['analytics_storage'],
},
functional: { // add your own category
label: 'Functional',
description: 'Chat widget, video embeds, saved preferences.',
required: false,
default: false,
consentModeSignals: ['functionality_storage', 'personalization_storage'],
},
},
// --- Callbacks (optional) ---
onInit: function (consent) {
// consent is the stored record, or null on first visit
},
onAcceptAll: function (consent) {},
onRejectAll: function (consent) {},
onSave: function (consent) {
// fired on EVERY save (accept-all, reject-all and custom)
if (consent.analytics) {
/* e.g. boot your own analytics here */
}
},
};
</script>
<!-- 2) BLOCKED TAGS — run only after the matching category is granted -->
<script type="text/plain" data-gdpr="analytics"
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<!-- 3) LOAD the library (auto-initialises from window.GDPRConfig) -->
<script src="https://cdn.jsdelivr.net/npm/freegdpr@1/dist/freegdpr.iife.min.js"></script>
</head>
<body>
<!-- Reopen the panel from anywhere -->
<footer>
<a href="#" onclick="GDPR.openPreferences(); return false;">Manage cookie preferences</a>
</footer>
</body>
</html>Minimal version — if you just want the defaults, the whole config is optional:
<script>window.GDPRConfig = { companyName: 'Acme Inc.' };</script> <script src="https://cdn.jsdelivr.net/npm/freegdpr@1/dist/freegdpr.iife.min.js"></script>
Public API — window.GDPR
| Method | Description |
| ------ | ----------- |
| getConsent() | Current record, or null if none. |
| hasConsent(category) | true/false for a category. |
| acceptAll() / rejectAll() | Apply and persist a blanket choice. |
| openPreferences() / closePreferences() | Toggle the preferences modal. |
| showBanner() | Force-show the banner. |
| reset(reload = true) | Clear stored consent (optionally reload). |
| on('consent', fn) | Subscribe to consent changes; returns an unsubscribe fn. |
| destroy() | Remove all DOM. |
| version | Library version string. |
DOM event
window.addEventListener('gdpr:consent', (e) => {
if (e.detail.analytics) initAnalytics();
});e.detail matches getConsent(): per-category booleans plus version, timestamp, consentId.
Accessibility
WCAG 2.1 AA: role="dialog" + aria-modal + aria-labelledby, focus trap, Esc to close with focus restore, labelled toggles, AA contrast in both themes, and prefers-reduced-motion support. The whole UI lives in a Shadow DOM so host-page styles can never break or be broken by it.
Development
npm install
npm run build # ESM + CJS + IIFE (+ .min) + .d.ts
npm test # vitest (jsdom)
npm run test:e2e # playwright (run `npx playwright install` first)
npm run typecheckOpen examples/demo.html after a build to try it live.
License
MIT © Daniele De Leon
