Lightweight, framework-agnostic cookie consent modal with Shadow DOM encapsulation, Google Consent Mode v2 support, i18n, and dark mode. Zero dependencies.
| Size | |
|---|---|
| Raw (ESM) | 16.4 KB |
| Raw (UMD) | 14.9 KB |
| Gzipped | ~5.1 KB |
- Shadow DOM encapsulation -- styles are fully isolated; no CSS leaks in or out
- Google Consent Mode v2 -- fires
consent defaultandconsent updatecommands automatically - i18n -- built-in locale files (en, es, de, fr) with auto-detection of browser language
- Dark mode --
true,false, or'auto'(followsprefers-color-scheme) - Block navigation -- prevent users from navigating away before giving consent
- Configurable categories -- any number of categories with optional
locked,default, andemojiproperties - Custom accent color -- single property to theme the modal
- Cookie persistence -- stores consent with
created_timestampandupdated_timestamp - HTML template partials -- override individual parts of the modal markup
- Custom events -- open the modal or settings panel from anywhere via
window.dispatchEvent - Callbacks --
onAcceptAll,onRejectAll,onSave,onChange - ESM + UMD builds -- works with bundlers,
<script>tags, jsdelivr, and unpkg - Zero dependencies
- MIT licensed
npm install @analytics-debugger/consent-modalOr with other package managers:
yarn add @analytics-debugger/consent-modal
pnpm add @analytics-debugger/consent-modal
bun add @analytics-debugger/consent-modalimport { createConsentModal } from '@analytics-debugger/consent-modal'
const modal = createConsentModal({
categories: [
{ key: 'necessary', label: 'Essential', emoji: '🛡️', description: 'Required for the site to function.', locked: true, default: true },
{ key: 'analytics', label: 'Analytics', emoji: '📊', sublabel: 'Performance', description: 'Helps us understand how the site is used.' },
{ key: 'marketing', label: 'Marketing', emoji: '🎯', sublabel: 'Targeting', description: 'Used for personalized advertising.' },
],
privacyPolicyUrl: '/privacy',
accentColor: '#c6ff00',
darkMode: 'auto',
onAcceptAll: (state) => console.log('Accepted all:', state),
onChange: (state) => console.log('Consent changed:', state),
})The modal will appear automatically on first visit. Once the user makes a choice, the consent state is persisted in a cookie and the modal will not appear again until the cookie expires.
All options are passed to createConsentModal(options).
| Option | Type | Description |
|---|---|---|
categories |
ConsentCategory[] |
Array of consent categories to display. |
| Option | Type | Default | Description |
|---|---|---|---|
cookieName |
string |
'cm_consent' |
Name of the cookie used to persist consent. |
cookieDays |
number |
365 |
Cookie expiration in days. |
privacyPolicyUrl |
string |
-- | URL to link in the footer. |
logoUrl |
string |
-- | Path to a logo image displayed in the modal header. |
accentColor |
string |
-- | CSS color applied to buttons and toggles. |
darkMode |
boolean | 'auto' |
-- | Enable dark mode. 'auto' follows the user's OS preference. |
blockNavigation |
boolean |
false |
Prevent navigation (beforeunload + popstate) while the modal is open. |
autoShow |
boolean |
true |
Automatically show the modal if no consent cookie is found. |
locale |
string |
'en' |
Active locale key. |
locales |
Record<string, ConsentLocale> |
-- | Locale data keyed by language code. |
detectLocale |
boolean |
false |
Auto-detect the browser's language and select a matching locale. |
texts |
ConsentTexts |
-- | Override default UI strings (heading, buttons, etc.). |
gcmMappings |
GCMMapping |
See below | Map GCM storage types to your category keys. |
onAcceptAll |
(state) => void |
-- | Called when the user accepts all categories. |
onRejectAll |
(state) => void |
-- | Called when the user rejects non-essential categories. |
onSave |
(state) => void |
-- | Called when the user saves custom choices. |
onChange |
(state) => void |
-- | Called on any consent change (accept, reject, or save). |
interface ConsentCategory {
key: string // Unique identifier (e.g. 'analytics')
label: string // Display name
description: string // Longer explanation shown in the settings panel
sublabel?: string // Secondary label (e.g. 'Performance Cookies')
emoji?: string // Emoji displayed next to the label
locked?: boolean // If true, the toggle is always on and cannot be disabled
default?: boolean // Initial state when no consent cookie exists
}All text strings are optional. Provide only the ones you want to override.
interface ConsentTexts {
heading?: string // Main heading
subheading?: string // Subheading below the main heading
descriptionP1?: string // First paragraph
descriptionP2?: string // Second paragraph
acceptAll?: string // Accept all button label
rejectAll?: string // Reject all button label
customize?: string // "Let me choose" button label
customizeHeading?: string // Settings panel heading
customizeSubheading?: string // Settings panel subheading
saveChoices?: string // Save button label
back?: string // Back button label
footerText?: string // Footer text before privacy link
privacyPolicyLink?: string // Privacy policy link text
}Built-in locale files are included for en, es, de, and fr under the i18n/ directory. You can also supply your own translations.
const modal = createConsentModal({
categories: [/* ... */],
locale: 'es',
locales: {
es: {
texts: {
heading: 'Tu privacidad importa',
acceptAll: 'Aceptar todo',
rejectAll: 'Rechazar todo',
customize: 'Personalizar',
saveChoices: 'Guardar preferencias',
},
categories: {
necessary: { label: 'Esenciales', description: 'Necesarias para el funcionamiento del sitio.' },
analytics: { label: 'Analítica', sublabel: 'Rendimiento', description: 'Nos ayudan a mejorar el sitio.' },
marketing: { label: 'Marketing', sublabel: 'Segmentación', description: 'Para publicidad personalizada.' },
},
},
},
})Set detectLocale: true to automatically select a locale based on the browser's navigator.language. The library checks for an exact match first (pt-BR), then falls back to the base language (pt), and finally defaults to 'en'.
createConsentModal({
categories: [/* ... */],
detectLocale: true,
locales: { es: { /* ... */ }, fr: { /* ... */ } },
})modal.setLocale('fr')
modal.show() // Will render in French// Always dark
createConsentModal({ categories: [/* ... */], darkMode: true })
// Always light
createConsentModal({ categories: [/* ... */], darkMode: false })
// Follow OS preference (prefers-color-scheme)
createConsentModal({ categories: [/* ... */], darkMode: 'auto' })When blockNavigation: true, the modal prevents the user from navigating away (via beforeunload and popstate) until consent is given. Navigation is restored as soon as the user accepts, rejects, or saves their choices.
createConsentModal({
categories: [/* ... */],
blockNavigation: true,
})The library automatically pushes consent default and consent update commands to dataLayer based on the user's choices. Map each GCM storage type to one of your category keys:
createConsentModal({
categories: [
{ key: 'necessary', label: 'Essential', description: '...', locked: true, default: true },
{ key: 'analytics', label: 'Analytics', description: '...' },
{ key: 'marketing', label: 'Marketing', description: '...' },
],
gcmMappings: {
ad_storage: 'marketing',
analytics_storage: 'analytics',
ad_user_data: 'marketing',
ad_personalization: 'marketing',
functionality_storage: 'necessary',
personalization_storage: 'necessary',
security_storage: 'necessary',
},
})If you omit gcmMappings, the following defaults are used:
{
ad_storage: 'marketing',
analytics_storage: 'analytics',
ad_user_data: 'marketing',
ad_personalization: 'marketing',
}The modal UI is built from HTML partials located in templates/parts/. The available partials are:
header.html-- logo and close button areadescription.html-- heading, subheading, and body textactions-main.html-- accept all, reject all, and customize buttonscategory.html-- individual category toggle rowactions-details.html-- save choices and back buttonsfooter.html-- privacy policy linkclose.html-- close button icondecoration.html-- decorative elements
Load the UMD build directly from a CDN. No bundler required.
<script src="https://cdn.jsdelivr.net/npm/@analytics-debugger/consent-modal/dist/dta-cm.umd.js"></script>
<script>
var modal = ConsentModal.createConsentModal({
categories: [
{ key: 'necessary', label: 'Essential', description: 'Required.', locked: true, default: true },
{ key: 'analytics', label: 'Analytics', description: 'Usage data.' },
],
privacyPolicyUrl: '/privacy',
})
</script><script src="https://unpkg.com/@analytics-debugger/consent-modal/dist/dta-cm.umd.js"></script>When using the consent modal with GTM, consent defaults must fire before GTM loads. The recommended approach is a two-part setup:
Add this script in the <head> before your GTM container snippet. This ensures all tags start in a denied state, and any previously saved consent is applied immediately.
<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',
wait_for_update:500
});
var m=document.cookie.match(/(?:^|; )my_consent=([^;]*)/);
if(m){try{var c=JSON.parse(decodeURIComponent(m[1]));
gtag('consent','update',{
ad_storage:c.marketing?'granted':'denied',
analytics_storage:c.analytics?'granted':'denied',
ad_user_data:c.marketing?'granted':'denied',
ad_personalization:c.marketing?'granted':'denied'
})}catch(e){}}
</script>
<!-- GTM snippet goes here -->Replace my_consent with your cookieName value, and adjust the category key mappings (c.marketing, c.analytics) to match your configuration.
Create a Custom HTML tag in GTM that fires on All Pages with high priority:
<script>
(function(){
var s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/@analytics-debugger/consent-modal/dist/dta-cm.umd.js';
s.onload = function(){
ConsentModal.createConsentModal({
categories: [
{ key: 'necessary', label: 'Essential', emoji: '\ud83d\udee1\ufe0f', description: 'Required for the site to work.', locked: true, default: true },
{ key: 'analytics', label: 'Analytics', emoji: '\ud83d\udcca', description: 'Helps us improve the site.', sublabel: 'Performance' },
{ key: 'marketing', label: 'Marketing', emoji: '\ud83c\udfaf', description: 'Personalized advertising.', sublabel: 'Targeting' }
],
cookieName: 'my_consent',
privacyPolicyUrl: '/privacy',
accentColor: '#c6ff00',
autoShow: true
});
};
document.head.appendChild(s);
})();
</script>The library handles consent update calls automatically when the user interacts with the modal. GTM tags configured with consent checks will fire or hold based on the current state.
- Page loads, inline script sets
consent defaulttodeniedfor all storage types - If a saved cookie exists, the inline script immediately fires
consent updatewith the saved preferences - GTM loads and checks consent state -- tags that require consent will wait
- The consent modal library loads and shows the modal to first-time visitors
- When the user makes a choice, the library fires
consent updateand GTM tags respond accordingly
createConsentModal(options) returns an instance with the following methods:
Opens the consent modal (main view).
Opens the consent modal directly to the settings/customization panel.
Closes the modal with a transition animation.
Returns the current consent state as a plain object.
modal.getState()
// { necessary: true, analytics: false, marketing: false }Returns true if the given category is currently granted.
modal.isGranted('analytics') // falseChanges the active locale. If the modal is currently rendered, it will be destroyed and rebuilt on the next show() call.
Returns the current locale string.
Removes the modal from the DOM and cleans up event listeners.
You can open the modal from anywhere in your application by dispatching custom events on window:
// Open the main consent view
window.dispatchEvent(new Event('consent-modal:open'))
// Open the settings/customization view directly
window.dispatchEvent(new Event('consent-modal:settings'))This is useful for "Manage cookies" links in footers or settings pages:
<a href="#" onclick="window.dispatchEvent(new Event('consent-modal:settings')); return false;">
Manage cookie preferences
</a>The consent cookie is stored as JSON with the following shape:
{
"necessary": true,
"analytics": false,
"marketing": false,
"created_timestamp": 1710000000000,
"updated_timestamp": 1710000000000
}The created_timestamp is set once on first consent. The updated_timestamp is refreshed every time the user changes their preferences.
Works in all modern browsers (Chrome, Firefox, Safari, Edge). Shadow DOM is required -- IE11 is not supported.
MIT -- Copyright (c) 2026 Analytics Debugger S.L.U.