GDPR & IMY 2026 compliant cookie consent Web Component. Shadow DOM encapsulated, framework-agnostic, under 8KB gzipped.
- Web Component — works in any framework (Astro, React, Vue, Angular, plain HTML)
- Shadow DOM — zero style conflicts with your site
- IMY 2026 compliant by default — symmetric buttons, no dark patterns
- Script-gate engine — blocks scripts, iframes, and tracking pixels until consent
- Google Consent Mode V2 — built-in gtag integration
- i18n — English and Swedish included, fully customisable
- 10 integration presets — GA4, GTM, Meta Pixel, LinkedIn, TikTok, Umami, Plausible, Matomo, HubSpot, Hotjar
- Multi-tenant dashboard — manage multiple domains with team collaboration
- Consent proof ledger — immutable records with PDF export via Gotenberg
- Agency mode — manage client organisations with white-label branding
- Billing integration — Mollie payments with subscription lifecycle
- Automated alerts — health scores, scheduled reports, email notifications
- Cookie scanner — automatic third-party cookie detection
- 7.5KB gzipped core bundle
<script src="https://consent.brightinteraction.com/loader.js"
data-domain="yourdomain.com"></script>The loader script fetches your published configuration and initialises the banner automatically.
npm install cookieproofimport 'cookieproof';
const cc = document.querySelector('cookie-consent');
cc.configure({
language: 'sv',
gcmEnabled: true,
});Mark scripts you want to gate behind consent:
<!-- Analytics (blocked until user consents to 'analytics') -->
<script type="text/plain" data-consent="analytics"
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX">
</script>
<!-- Marketing (blocked until user consents to 'marketing') -->
<script type="text/plain" data-consent="marketing">
fbq('init', '1234567890');
fbq('track', 'PageView');
</script>
<!-- Iframes -->
<iframe data-consent="marketing"
data-src="https://www.youtube.com/embed/dQw4w9WgXcQ"
style="display:none;">
</iframe>When the user grants consent for a category, the engine swaps type="text/plain" to type="text/javascript" and the scripts execute.
cc.configure({
// Categories (defaults: necessary, analytics, marketing, preferences)
categories: [
{ id: 'necessary', required: true, enabled: true },
{ id: 'analytics', required: false, enabled: false },
{ id: 'marketing', required: false, enabled: false },
{ id: 'preferences', required: false, enabled: false },
],
// UI
position: 'bottom', // 'bottom' | 'top' | 'center'
theme: 'auto', // 'light' | 'dark' | 'auto'
floatingTrigger: 'left', // true | false | 'left' | 'right'
// i18n
language: 'sv', // 'en' | 'sv' | auto-detect
// Storage
storage: 'localStorage', // 'localStorage' | 'cookie'
cookieExpiry: 365, // days
// Versioning (bump to re-prompt users)
revision: 1,
// Google Consent Mode V2
gcmEnabled: true,
// Callbacks
onAccept: (consent) => console.log('Accepted:', consent),
onReject: (consent) => console.log('Rejected:', consent),
onChange: (consent, changed) => console.log('Changed:', changed),
});const cc = document.querySelector('cookie-consent');
// Actions
cc.acceptAll();
cc.rejectAll();
cc.acceptCategory('analytics');
cc.rejectCategory('marketing');
// State
cc.getConsent(); // ConsentRecord | null
cc.hasConsent('analytics'); // boolean
// UI
cc.showBanner();
cc.showPreferences();
cc.hide();
cc.reset(); // Clear consent, show banner
// Events
const unsub = cc.on('consent:update', (detail) => {
console.log(detail.consent);
console.log(detail.changed);
});
unsub(); // unsubscribe| Event | When |
|---|---|
consent:init |
Component initialised, consent state loaded |
consent:update |
Any consent change |
consent:accept-all |
User clicked Accept All |
consent:reject-all |
User clicked Reject All |
consent:category:analytics |
Analytics category changed |
The API supports two authentication methods:
- Session cookies — The configurator dashboard uses httpOnly cookies with CSRF double-submit tokens. Login via
POST /api/auth/login. - API key — Set
COOKIEPROOF_API_KEYand pass asAuthorization: Bearer <key>. Used for headless/CI integrations.
Public endpoints (config lookup, proof recording, health check) require no authentication.
| Method | Path | Description |
|---|---|---|
GET |
/api/config/:domain |
Fetch published banner config for a domain |
POST |
/api/proof |
Record a consent proof (called by the widget) |
GET |
/api/health |
Health check (database + Gotenberg status) |
| Method | Path | Description |
|---|---|---|
GET |
/api/proof |
List proofs with pagination and filters |
GET |
/api/proof/:id |
Get a single proof by UUID |
GET |
/api/proof/stats |
Aggregate stats + daily breakdown |
GET |
/api/proof/export |
CSV export of matching proofs |
DELETE |
/api/proof |
Purge proofs older than a timestamp |
domain— filter by domain (exact match)method— filter by consent method (accept-all,reject-all,custom,gpc)from/to— timestamp range (milliseconds since epoch)limit/offset— pagination (max 500)
Set WEBHOOK_URL to receive a POST notification whenever a new consent proof is recorded:
{
"event": "consent.recorded",
"data": { "id": "...", "domain": "...", "method": "accept-all", "categories": {} },
"timestamp": 1234567890
}Set WEBHOOK_SECRET to include an X-Webhook-Secret header for verification.
git clone <repo-url>
cd CookieProof
cp .env.example .env # configure all required variables
docker compose up -d --buildSee .env.example for a complete reference. Key variables:
| Variable | Default | Description |
|---|---|---|
COOKIEPROOF_API_KEY |
(empty) | API key for Bearer token authentication |
ALLOWED_ORIGINS |
(empty) | Comma-separated allowed CORS origins |
RETENTION_DAYS |
1095 (3 years) |
Auto-purge proofs older than this |
WEBHOOK_URL |
(empty) | URL to POST consent events to |
WEBHOOK_SECRET |
(empty) | Shared secret for webhook verification |
RESEND_API_KEY |
(empty) | Resend API key (preferred email provider) |
SMTP_HOST |
(empty) | SMTP server for email (fallback) |
ADMIN_EMAIL |
(empty) | Super admin email for bootstrap |
MOLLIE_API_KEY |
(empty) | Mollie payment API key |
MOLLIE_WEBHOOK_URL |
(empty) | Mollie webhook callback URL (HTTPS) |
MOLLIE_REDIRECT_URL |
(empty) | Redirect after Mollie checkout (HTTPS) |
GOTENBERG_URL |
http://gotenberg:3000 |
Gotenberg PDF service URL |
┌─────────────┐ ┌──────────┐ ┌────────────┐ ┌────────────┐
│ Browser │───▶│ nginx │───▶│ Bun API │───▶│ Gotenberg │
│ <cookie- │ │ :8080 │ │ :3100 │ │ :3000 │
│ consent> │ │ /dist/ │ │ /api/* │ │ PDF gen │
│ │ │ /config │ │ SQLite │ │ │
└─────────────┘ └──────────┘ └────────────┘ └────────────┘
│ /loader.js │
│ /configurator/ │
└──────────────────────┘
Services are deployed via Docker Compose with:
- Read-only filesystems and minimal tmpfs mounts
- All capabilities dropped (
cap_drop: ALL) no-new-privilegessecurity option- Memory and CPU resource limits
- Non-root containers (nginx-unprivileged, su-exec user drop)
import { ga4, facebookPixel } from 'cookieproof/integrations';
cc.configure({
categories: [
{ id: 'necessary', required: true, enabled: true },
{
id: 'analytics',
services: [ga4('G-XXXXX').service],
},
{
id: 'marketing',
services: [facebookPixel('1234567890').service],
},
],
gcmEnabled: true,
});Available presets: ga4, gtm, facebookPixel, linkedinInsight, tiktokPixel, umami, plausible, matomo, hubspot, hotjar.
CSS custom properties pierce through Shadow DOM:
cookie-consent {
--cc-bg: #ffffff;
--cc-text: #1a1a1a;
--cc-text-secondary: #6b7280;
--cc-border: #e5e7eb;
--cc-btn-primary-bg: #0d9488;
--cc-btn-primary-text: #ffffff;
--cc-btn-secondary-bg: #e5e7eb;
--cc-btn-secondary-text: #374151;
--cc-toggle-on: #0d9488;
--cc-radius: 12px;
--cc-font: system-ui, sans-serif;
--cc-z-index: 10000;
--cc-max-width: 540px;
}::part() selectors are exposed for deeper customisation: banner, preferences, trigger, btn, btn-accept, btn-reject, btn-settings, category, category-toggle.
These are enforced by default — you don't need to configure anything:
- Reject All button on the first layer with equal visual prominence to Accept All
- No pre-ticked category checkboxes
- Persistent floating trigger for changing consent after the banner is dismissed
- Button order: Reject / Settings / Accept (no positional bias)
- Console warning if you override compliance defaults
When gcmEnabled: true, the engine automatically:
- Calls
gtag('consent', 'default', { all signals: 'denied' })on page load - Updates signals when the user makes a choice:
analyticscategory →analytics_storagemarketingcategory →ad_storage,ad_user_data,ad_personalizationpreferencescategory →functionality_storage,personalization_storage
You can override the mapping with gcmMapping.
When respectGPC is true (default) and navigator.globalPrivacyControl === true:
- Non-essential categories are automatically rejected
- No banner is shown (the floating trigger appears for manual override)
- A
consent:gpcevent is emitted - The proof record has
method: 'gpc'
Set geoEndpoint to skip the consent banner for visitors outside regulated regions:
cc.configure({
geoEndpoint: 'https://your-api.com/geo'
});The endpoint must return JSON: { "requiresConsent": true | false }. HTTPS is required. If the endpoint fails or is unreachable, the banner is shown (fail-safe).
MIT