Contents
CookieDialog is a lightweight, zero-dependency cookie consent dialog for GDPR compliance. It ships as a single ~11 KB script plus a small stylesheet, works from a CDN tag or an npm import, and includes optional IP geolocation so visitors outside the EU/EEA never see the banner. It powers the consent dialog on this site.
- GitHub: github.com/timothydodd/cookiedialog
- npm: npmjs.com/package/cookiedialog
- License: MIT
Quick start
Add the stylesheet and script, then call init():
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/cookiedialog.min.css">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cookiedialog.min.js"></script>
<script>
CookieDialog.init({
privacyUrl: '/privacy',
onAccept: (consent) => {
console.log('Consent given:', consent.categories);
}
});
</script>
That’s it — the dialog appears at the bottom of the page, remembers the visitor’s choice in localStorage, and skips itself on the next visit. CookieDialog.init() with no arguments works too; every option has a sensible default.
Installation
CDN
Pin a version (recommended for production):
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/cookiedialog.min.css">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cookiedialog.min.js"></script>
Or track the latest release with cookiedialog@latest in both URLs.
npm
npm install cookiedialog
import CookieDialog from 'cookiedialog';
import 'cookiedialog/dist/cookiedialog.min.css';
CookieDialog.init({ privacyUrl: '/privacy' });
The default export is an API object with init(config), create(config), and the CookieDialog class itself. init builds the dialog and starts it immediately; create constructs an instance without initializing so you can call .init() later:
import { CookieDialog } from 'cookiedialog';
const dialog = new CookieDialog({ theme: 'dark', autoShow: false });
await dialog.init();
dialog.show();
ES modules from the CDN
<script type="module">
import CookieDialog from 'https://cdn.jsdelivr.net/npm/cookiedialog@latest/dist/cookiedialog.esm.js';
CookieDialog.init();
</script>
Self-hosting
Download cookiedialog.min.js and cookiedialog.min.css from the GitHub releases page and serve them like any other static asset. This site does exactly that — no third-party CDN request required.
Configuration
All options are optional. Defaults shown are the values applied when the option is omitted.
| Option | Type | Default | Description |
|---|---|---|---|
autoShow |
boolean |
true |
Show the dialog automatically during init(). Set false to call show() yourself. |
position |
string |
'bottom' |
'bottom', 'top', or 'center'. Center renders as a modal with a click-to-dismiss overlay. |
theme |
string |
'light' |
'light' or 'dark'. |
privacyUrl |
string |
— | If set, a privacy policy link is rendered inside the dialog text. |
cookiePolicyUrl |
string |
— | Same as privacyUrl, for a separate cookie policy page. |
expiryDays |
number |
365 |
How long stored consent is honored. Expired consent is cleared and the dialog shows again. |
forceShow |
boolean |
false |
Show the dialog even when valid consent is already stored (and even for non-EU visitors when geolocation is on). |
enableLocation |
boolean |
false |
Look up the visitor’s country by IP and skip the dialog outside the EU/EEA. See Geolocation. |
geolocationEndpoint |
string |
'https://ipapi.co/json/' |
Custom geolocation API URL. See Custom endpoints for the required response shape. |
categories |
array |
3 built-ins | Cookie categories shown in the settings panel. See Categories. |
translations |
object |
English strings | All user-facing text. See Translations. |
onAccept |
function |
— | Called with the consent state when the visitor accepts, when valid stored consent is found on load, and after a location-based auto-accept. |
onReject |
function |
— | Called (with no arguments) when the visitor clicks Reject All. |
onChange |
function |
— | Called with the consent state when the visitor saves custom settings. |
onLocationNotRequired |
function |
— | Called with the location result when geolocation determines consent isn’t needed. |
Cookie categories
Three categories are built in:
| Id | Name | Required |
|---|---|---|
necessary |
Necessary | yes — always on, checkbox disabled |
analytics |
Analytics | no |
marketing |
Marketing | no |
Replace them with your own by passing categories. Every field is required — including description:
CookieDialog.init({
categories: [
{
id: 'necessary',
name: 'Essential Cookies',
description: 'Required for the website to function',
required: true
},
{
id: 'analytics',
name: 'Analytics',
description: 'Help us understand how you use our site',
required: false
},
{
id: 'ads',
name: 'Advertising',
description: 'Used for targeted advertising',
required: false
}
]
});
Categories with required: true are always saved as true, even when the visitor clicks Reject All.
Translations
Pass translations to localize or reword the dialog. The object replaces the defaults rather than merging with them, so supply every key you need:
CookieDialog.init({
translations: {
title: 'Cookie Settings',
description: 'We use cookies to enhance your browsing experience and analyze our traffic. Please choose your preferences.',
acceptButton: 'Accept All',
rejectButton: 'Reject All',
settingsButton: 'Cookie Settings',
closeButton: 'Save Settings',
privacyLink: 'Privacy Policy',
cookiePolicyLink: 'Cookie Policy',
necessaryCategory: 'Necessary',
necessaryDescription: 'Essential cookies for the website to function properly'
}
});
The values above are the defaults. necessaryCategory and necessaryDescription rename the built-in required category; closeButton labels the save button in the settings panel. No locale bundles ship with the library — detect the language yourself (for example from navigator.language) and pass the matching strings.
Geolocation
With enableLocation: true, init() calls an IP geolocation API (by default ipapi.co) before showing anything:
- Visitor in the EU/EEA or UK — the dialog shows normally.
- Visitor elsewhere — all categories are auto-accepted with reason
location_not_required,onLocationNotRequiredthenonAcceptfire, and the dialog never renders. - Lookup fails — CookieDialog fails safe: it assumes consent is required and shows the dialog.
CookieDialog.init({
enableLocation: true,
onLocationNotRequired: (location) => {
console.log('Consent not required for', location.country);
},
onAccept: (consent) => {
if (consent.reason === 'location_not_required') {
// auto-accepted by location, consent.locationData has the details
}
}
});
Results are cached in memory for one hour. The region list covers the 27 EU member states plus the UK, Iceland, Liechtenstein, and Norway.
Two things to keep in mind:
- The visitor’s IP address is sent to the geolocation service — mention that in your privacy policy, or leave
enableLocationoff (the default) so every visitor simply sees the dialog. - ipapi.co’s free tier is rate-limited; for real traffic use a paid plan or your own endpoint.
Custom endpoints
Point geolocationEndpoint at your own API to keep lookups first-party. Custom endpoints must return an inEU (or in_eu) boolean — a response with only a country code is treated as outside the EU:
{ "inEU": true, "country": "Germany", "region": "Bavaria" }
CookieDialog.init({
enableLocation: true,
geolocationEndpoint: 'https://your-api.example.com/geo'
});
API reference
CookieDialog.init(config) returns the dialog instance. All methods return plain values — there is no chaining.
| Method | Returns | Description |
|---|---|---|
init() |
Promise<void> |
Checks stored consent, runs geolocation if enabled, then shows the dialog (unless autoShow: false). |
show() |
void |
Shows the dialog, rendering it first if needed. |
hide() |
void |
Hides the dialog (the DOM stays in place). |
destroy() |
void |
Removes the dialog and overlay from the DOM. |
getConsent() |
object | null |
The stored consent state, or null if missing or expired. |
hasConsent() |
boolean |
Whether valid consent is stored. |
getCategoryConsent(id) |
boolean |
Whether a specific category was accepted. |
resetConsent() |
void |
Clears stored consent. Call show() afterwards to re-prompt. |
const dialog = CookieDialog.init({ autoShow: false });
if (!dialog.hasConsent()) {
dialog.show();
}
if (dialog.getCategoryConsent('analytics')) {
// load your analytics script
}
Consent state
getConsent(), onAccept, and onChange all deal in the same object:
{
"timestamp": 1640995200000,
"categories": { "necessary": true, "analytics": false, "marketing": true },
"version": "1.0.0",
"reason": "user_accept",
"locationData": {
"country": "US",
"region": "California",
"inEU": false,
"detectionMethod": "ip_geolocation"
}
}
reason is user_accept, user_reject, or location_not_required. locationData is present only when geolocation made the decision.
Storage
Consent lives in localStorage under the key cookiedialog_consent — the library itself sets no cookies. Consent expires expiryDays after it was saved, and a stored schema version guards against format changes. To reset during testing:
localStorage.removeItem('cookiedialog_consent');
Styling
The dialog is styled by plain CSS classes — override any .cookie-dialog-* rule in your own stylesheet to restyle it. The most useful hooks:
| Class | Element |
|---|---|
.cookie-dialog |
Root container (also carries position-* and theme-* classes) |
.cookie-dialog-title / .cookie-dialog-description |
Heading and body text |
.cookie-dialog-button |
All buttons |
.cookie-dialog-button-accept / -reject / -settings |
Individual buttons |
.cookie-dialog-settings |
The expandable settings panel |
.cookie-dialog-category |
One category row |
.cookie-dialog-toggle-slider |
The toggle switch |
.cookie-dialog-overlay |
Backdrop for position: 'center' |
For example, matching the accept button to a site accent color:
.cookie-dialog-button-accept {
background: #C2368F;
}
.cookie-dialog-button-accept:hover {
background: #a52c79;
}
The dialog uses z-index: 999999, stacks buttons vertically on screens under 768px wide, and animates in with a 0.3s slide or fade depending on position.
Loading analytics on consent
The main use case: keep tracking scripts unloaded until the visitor opts in. With Google Analytics 4 and Consent Mode v2, deny everything by default, then update from the dialog’s callbacks:
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
analytics_storage: 'denied',
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
wait_for_update: 500
});
</script>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
function applyConsent(consent) {
gtag('consent', 'update', {
analytics_storage: consent.categories.analytics ? 'granted' : 'denied',
ad_storage: consent.categories.marketing ? 'granted' : 'denied',
ad_user_data: consent.categories.marketing ? 'granted' : 'denied',
ad_personalization: consent.categories.marketing ? 'granted' : 'denied'
});
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
}
CookieDialog.init({
onAccept: applyConsent,
onChange: applyConsent,
onReject: () => {
// defaults already deny everything
}
});
</script>
The same pattern works for Microsoft Clarity (clarity('consentv2', …)), Facebook Pixel, chat widgets, and anything else that should wait for a yes: put the loader inside a function and call it from onAccept/onChange when the relevant category is true.
TypeScript
Type declarations ship with the package:
import CookieDialog from 'cookiedialog';
import type { CookieDialogConfig, ConsentState } from 'cookiedialog';
const config: CookieDialogConfig = {
theme: 'dark',
onAccept: (consent: ConsentState) => {
console.log(consent.categories);
}
};
CookieDialog.init(config);
CookieCategory, Translations, and GeolocationResponse are exported from the package root as well.
Single-page apps and SSR
CookieDialog touches document and localStorage, so initialize it client-side only — in a useEffect (React), onMounted (Vue), or afterNextRender (Angular). One dialog instance per page: the rendered elements use fixed ids, so create it once at app startup rather than per component.
useEffect(() => {
const dialog = CookieDialog.init({ autoShow: true });
return () => dialog.destroy();
}, []);
Notes and limits
- Config strings (
title,description, category names, URLs) are injected into the dialog’s HTML without escaping — never pass untrusted user input into the config. - With
enableLocationon, the library logs its geolocation progress to the console. - Browser support: Chrome/Edge 88+, Firefox 78+, Safari 14+, Opera 74+.
Get help
Bugs and feature requests: GitHub issues. The repo also includes a manual test page (test.html) that exercises every option and callback — handy for trying settings before wiring them into your site.