RoboDodd
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.

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, onLocationNotRequired then onAccept fire, 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 enableLocation off (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
}

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 enableLocation on, 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.