@analytics-debugger/consent-modal
v1.0.1
Published
Lightweight cookie consent modal with Shadow DOM encapsulation, Google Consent Mode v2, i18n, and dark mode. Zero dependencies, ~6KB gzipped.
Maintainers
Readme
@analytics-debugger/consent-modal
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 |
Features
- 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
Installation
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-modalQuick Start
import { 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.
Configuration
All options are passed to createConsentModal(options).
Required
| Option | Type | Description |
|---|---|---|
| categories | ConsentCategory[] | Array of consent categories to display. |
Optional
| 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). |
ConsentCategory
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
}ConsentTexts
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
}i18n
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.' },
},
},
},
})Auto-detection
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: { /* ... */ } },
})Changing locale at runtime
modal.setLocale('fr')
modal.show() // Will render in FrenchDark Mode
// Always dark
createConsentModal({ categories: [/* ... */], darkMode: true })
// Always light
createConsentModal({ categories: [/* ... */], darkMode: false })
// Follow OS preference (prefers-color-scheme)
createConsentModal({ categories: [/* ... */], darkMode: 'auto' })Block Navigation
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,
})Google Consent Mode v2
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',
},
})Default mappings
If you omit gcmMappings, the following defaults are used:
{
ad_storage: 'marketing',
analytics_storage: 'analytics',
ad_user_data: 'marketing',
ad_personalization: 'marketing',
}Custom Templates
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
CDN Usage
Load the UMD build directly from a CDN. No bundler required.
jsdelivr
<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>unpkg
<script src="https://unpkg.com/@analytics-debugger/consent-modal/dist/dta-cm.umd.js"></script>Google Tag Manager Integration
When using the consent modal with GTM, consent defaults must fire before GTM loads. The recommended approach is a two-part setup:
Step 1: Inline consent defaults (before GTM snippet)
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.
Step 2: Load the modal via GTM Custom HTML tag
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.
How it works
- 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
API Reference
createConsentModal(options) returns an instance with the following methods:
modal.show()
Opens the consent modal (main view).
modal.showSettings()
Opens the consent modal directly to the settings/customization panel.
modal.hide()
Closes the modal with a transition animation.
modal.getState()
Returns the current consent state as a plain object.
modal.getState()
// { necessary: true, analytics: false, marketing: false }modal.isGranted(key)
Returns true if the given category is currently granted.
modal.isGranted('analytics') // falsemodal.setLocale(locale)
Changes the active locale. If the modal is currently rendered, it will be destroyed and rebuilt on the next show() call.
modal.getLocale()
Returns the current locale string.
modal.destroy()
Removes the modal from the DOM and cleans up event listeners.
Events
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>Cookie Format
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.
Browser Support
Works in all modern browsers (Chrome, Firefox, Safari, Edge). Shadow DOM is required -- IE11 is not supported.
License
MIT -- Copyright (c) 2026 Analytics Debugger S.L.U.
