Skip to content

Repository files navigation

Keksmeister Logo

Keksmeister

Der Meister der Kekse — A modern, framework-agnostic cookie consent Web Component.

npm version CI license demo

Keksmeister is a lightweight cookie consent library built as a Web Component with Shadow DOM encapsulation. It drops into any web application — Angular, React, Vue, Svelte, or plain HTML — without style conflicts or framework dependencies.

Built for footage.one with GDPR/DSGVO compliance as a first-class concern. Live Demo

Why another cookie consent library?

We evaluated every major open-source consent library and found that none combines all three properties we needed:

Library Web Component Full GDPR Features Modern ESM + TS
orestbida/cookieconsent
Porsche cookie-consent-banner ⚠️ partial
Klaro
tarteaucitron
c15t ✅ (but Next.js focused)
Keksmeister

Inspiration

  • orestbida/cookieconsent — Best-in-class feature set (granular consent, script blocking, revision system, Google Consent Mode). We modeled our consent engine after this library's approach.
  • Porsche cookie-consent-banner — Proved that a StencilJS-based Web Component approach works for consent banners. We chose native Custom Elements over Stencil to eliminate the runtime dependency.
  • Klaro — Config-driven approach with per-service consent and data-attribute script blocking. Our data-keksmeister attribute pattern is inspired by Klaro's design.
  • Orejime — Accessibility-focused Klaro fork. Reminded us that a11y is non-negotiable for consent UIs.
  • c15t — Modern developer experience and full-stack consent logging. We agree on the DX goal but keep the server-side optional (a simple callback, not a whole database).

Features

  • Web Component<keksmeister-banner> with Shadow DOM encapsulation
  • Zero dependencies — Vanilla TypeScript, no framework runtime
  • Tiny bundle — Target < 8 kB gzipped
  • GDPR/DSGVO compliant — Accept All + Reject All on first layer (equal prominence), granular categories, consent versioning
  • Script blockingdata-keksmeister attribute blocks scripts until consent
  • Service adapters — Programmatic opt-in/opt-out for PostHog, Matomo, GA4, Meta Pixel, Mixpanel, HubSpot, Plausible, TikTok Pixel
  • Auto-clear cookies — Revoked categories' cookies are automatically removed
  • Google Consent Mode v2 — Built-in gtag('consent', 'update', ...) integration
  • CSS Custom Properties — Fully themeable from outside Shadow DOM
  • i18n — German and English built-in, extensible with custom translations
  • Accessible — Keyboard navigation, focus management, ARIA attributes
  • Consent proofonConsent callback for server-side logging (you own the endpoint), plus an optional minimal file-based logging server in server/ (Bun + Docker, no database)

Quick Start

npm

npm install keksmeister
import 'keksmeister';
<keksmeister-banner
  privacy-url="/datenschutz"
  lang="de"
></keksmeister-banner>

CDN / Script Tag

No build step needed. Pick your preferred CDN:

<!-- jsDelivr (recommended) -->
<script type="module" src="https://cdn.jsdelivr.net/npm/keksmeister/dist/keksmeister.min.js"></script>

<!-- or unpkg -->
<script type="module" src="https://unpkg.com/keksmeister/dist/keksmeister.min.js"></script>

<keksmeister-banner
  privacy-url="/datenschutz"
  lang="de"
></keksmeister-banner>
<keksmeister-trigger position="bottom-left"></keksmeister-trigger>

Pin a specific version for production:

<script type="module" src="https://cdn.jsdelivr.net/npm/keksmeister@0.3.0/dist/keksmeister.min.js"></script>

Browse all available files: keksmeister on jsDelivr

JavaScript Configuration

import { KeksmeisterBanner } from 'keksmeister';

const banner = document.querySelector('keksmeister-banner');
banner.config = {
  categories: [
    { id: 'essential', label: 'Essential', required: true },
    { id: 'analytics', label: 'Analytics' },
    { id: 'marketing', label: 'Marketing' },
  ],
  privacyUrl: '/datenschutz',
  lang: 'de',
  revision: '2',
  googleConsentMode: true,
  onConsent: (record) => {
    // Send consent proof to your backend
    fetch('/api/consent', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(record),
    });
  },
};

Server-side consent logging (proof / Nachweispflicht)

DSGVO / TDDDG require you to prove consent. Every ConsentRecord carries the fields you need for that: a timestamp, the banner revision, the choices, the method, an action (grant / update / revoke) and a pseudonymous, cookie-persisted subjectId that ties a visitor's decisions together without storing any personal data.

You can ship records to your backend in two ways:

  1. onConsent callback — full control, you own the transport (fires on grant/update).
  2. Built-in logger (logging) — set an endpoint and Keksmeister sends every decision including revocations via navigator.sendBeacon (so it survives the navigation after "Accept all"), with a localStorage retry queue for offline/failed sends:
banner.config = {
  // ...categories, privacyUrl, revision...
  logging: {
    endpoint: 'https://consent.example.com/consent',
    includeUserAgent: false, // opt-in; raw IP is never sent by the library
  },
};

A ready-to-run, database-free endpoint for this is included in server/ (Bun + Docker; stores each record as a plain JSON file, ZIP the folder for a regulator export).

See the Consent Proof & Server-Side Logging guide for the full logging options, audit fields, CORS configuration, and export details. For how Keksmeister maps to § 25 TDDDG, the DSK Orientierungshilfe, and the EinwV / § 26 TDDDG boundaries (Keksmeister is a self-hosted library, not an anerkannter Einwilligungsverwaltungsdienst), see docs/compliance.md.

Resource Blocking (data-keksmeister)

The blocker now covers every parse-time tracker the EDPB Guidelines 02/2023 v2.0 puts in scope, not just <script>. Same data-keksmeister attribute, same placeholder pattern (data-src / data-href):

<!-- JS analytics -->
<script type="text/plain" data-keksmeister="analytics" data-src="https://example.com/analytics.js"></script>

<!-- Inline script -->
<script type="text/plain" data-keksmeister="marketing">
  // This runs only after marketing consent
</script>

<!-- Tracking pixel (Meta Pixel, 1x1 counter, …) -->
<img data-keksmeister="marketing" data-src="https://example.com/pixel.gif" width="1" height="1" alt="" />

<!-- Embedded widget / map / video -->
<iframe data-keksmeister="marketing" data-src="https://www.youtube.com/embed/…" allowfullscreen></iframe>

<!-- preload / preconnect that fetches a third-party asset -->
<link data-keksmeister="analytics" rel="preload" as="style" data-href="https://example.com/asset.css">

The blocker also handles <source>, <video> and <audio>. <a> is intentionally not in scope: anchors only fetch on user click.

Service Adapters (Programmatic Consent)

Many modern analytics libraries (PostHog, Matomo, GA4, etc.) are initialized via JavaScript and have their own opt-in/opt-out APIs. For these, use service adapters instead of script blocking:

import { ConsentManager, ServiceRegistry } from 'keksmeister';
import { createPostHogAdapter } from 'keksmeister/adapters/posthog';
import { createGoogleAnalyticsAdapter } from 'keksmeister/adapters/google-analytics';
import { createMetaPixelAdapter } from 'keksmeister/adapters/meta-pixel';

// PostHog must be initialized with opt_out_capturing_by_default: true
posthog.init('phc_...', {
  api_host: 'https://eu.i.posthog.com',
  opt_out_capturing_by_default: true,
});

// The banner's ConsentManager is exposed after config is set
const banner = document.querySelector('keksmeister-banner');
const registry = new ServiceRegistry(banner.manager);

// Register adapters — they auto-sync with consent state
registry.register(createPostHogAdapter(posthog));
registry.register(createGoogleAnalyticsAdapter());
registry.register(createMetaPixelAdapter());

Available Adapters

Adapter Import Default Category
PostHog keksmeister/adapters/posthog analytics
Matomo keksmeister/adapters/matomo analytics
Google Analytics 4 keksmeister/adapters/google-analytics analytics
Google Ads keksmeister/adapters/google-analytics marketing
Meta Pixel keksmeister/adapters/meta-pixel marketing
Mixpanel keksmeister/adapters/mixpanel analytics
HubSpot keksmeister/adapters/hubspot marketing
Plausible keksmeister/adapters/plausible analytics
TikTok Pixel keksmeister/adapters/tiktok-pixel marketing

Adapter vs. Script Blocking — Which to use?

Not every tool has a consent API. Some must be blocked from loading entirely until consent is given. Here's a guide for common tools:

Tool Approach Reason
PostHog Adapter Has opt_in_capturing() / opt_out_capturing() API
Matomo Adapter Has requireConsent / setConsentGiven commands
Google Analytics 4 Adapter Google Consent Mode v2 (gtag('consent', 'update', ...))
Google Ads Adapter Shares Google Consent Mode v2 with GA4
Meta Pixel Adapter Has fbq('consent', 'grant') / fbq('consent', 'revoke')
Mixpanel Adapter Has opt_in_tracking() / opt_out_tracking() API
HubSpot Adapter Has _hsp.push(['setHubSpotConsent', ...]) queue
Plausible Adapter Uses localStorage.plausible_ignore flag
TikTok Pixel Adapter Has ttq.grantConsent() / ttq.revokeConsent()
Hotjar Script Blocking No consent API — block the script until consent
Google Tag Manager Script Blocking Load GTM only after consent, or use Consent Mode
Clarity (Microsoft) Script Blocking No consent API — block the script until consent
Intercom Script Blocking No granular consent API — block until consent
Crisp Chat Script Blocking No consent API — block until consent
YouTube Embeds Script Blocking Use data-keksmeister on the embed script
Google Maps Script Blocking Block iframe/script until consent
Custom Scripts Script Blocking Any <script> without its own consent API

Rule of thumb: If the tool has a JavaScript API to enable/disable tracking at runtime, use an adapter. If it only works by loading/not loading its script, use script blocking with data-keksmeister.

Script blocking example for Hotjar:

<script type="text/plain" data-keksmeister="analytics">
  (function(h,o,t,j,a,r){
    h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
    h._hjSettings={hjid:YOUR_ID,hjsv:6};
    a=o.getElementsByTagName('head')[0];
    r=o.createElement('script');r.async=1;
    r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
    a.appendChild(r);
  })(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
</script>

Custom Adapters

Write your own adapter for any service:

registry.register({
  id: 'my-analytics',
  category: 'analytics',
  onConsent: () => myAnalytics.enable(),
  onRevoke: () => myAnalytics.disable(),
});

Theming

keksmeister-banner {
  --km-primary: #0066cc;
  --km-primary-text: #ffffff;
  --km-bg: #1a1a1a;
  --km-text: #ffffff;
  --km-text-secondary: #aaaaaa;
  --km-border: #333333;
  --km-radius: 12px;
  --km-font-family: 'Inter', sans-serif;
}

Re-open Settings

<button onclick="document.querySelector('keksmeister-banner').openSettings()">
  Cookie-Einstellungen
</button>

Headless / Core Only

Use the consent engine without the UI:

import { ConsentManager, ScriptBlocker } from 'keksmeister/core';

const manager = new ConsentManager({
  categories: [
    { id: 'essential', label: 'Essential', required: true },
    { id: 'analytics', label: 'Analytics' },
  ],
  privacyUrl: '/privacy',
});

const blocker = new ScriptBlocker(manager);
blocker.start();

// Programmatic consent
manager.acceptAll();
manager.isAccepted('analytics'); // true

Architecture

src/
├── core/                      # Headless consent engine (no UI)
│   ├── consent-manager.ts     # Consent state machine + EventTarget
│   ├── cookie-store.ts        # Cookie read/write with base64 encoding
│   ├── script-blocker.ts      # MutationObserver-based script activation
│   ├── service-adapter.ts     # ServiceAdapter interface + ServiceRegistry
│   └── types.ts               # All TypeScript interfaces
│
├── adapters/                  # Built-in service adapters
│   ├── posthog.ts             # PostHog opt-in/opt-out
│   ├── matomo.ts              # Matomo requireConsent/setCookieConsentGiven
│   ├── google-analytics.ts    # GA4 Consent Mode v2 (analytics + ads)
│   ├── meta-pixel.ts          # Meta Pixel consent grant/revoke
│   ├── mixpanel.ts            # Mixpanel opt-in/opt-out tracking
│   ├── hubspot.ts             # HubSpot setHubSpotConsent
│   ├── plausible.ts           # Plausible localStorage flag
│   └── tiktok-pixel.ts        # TikTok Pixel grantConsent/revokeConsent
│
├── ui/                        # Web Component layer
│   ├── keksmeister-banner.ts  # <keksmeister-banner> Custom Element
│   └── styles.ts              # Shadow DOM CSS with Custom Properties
│
└── i18n/                      # Translations
    ├── de.ts                  # German (default)
    └── en.ts                  # English

Design Decisions

  • Native Custom Elements over Stencil/Lit — Zero runtime overhead. The component is simple enough that a framework adds complexity without proportional benefit.
  • Shadow DOM — Guarantees style encapsulation. The banner looks the same in every host application.
  • CSS Custom Properties — The only styling API that crosses the Shadow DOM boundary. All visual aspects are customizable without ::part() or ::slotted().
  • Core/UI split — The consent engine (ConsentManager) is usable headless. Build your own UI if needed.
  • Logging belongs in your backend — Consent proof logging lives in your infrastructure via the onConsent callback or the built-in logging transport. We keep the library focused but ship an optional, database-free logging server in server/ for those who want a turnkey option.
  • data-keksmeister attribute — Simple, declarative script blocking inspired by Klaro's data-name approach.
  • Two consent mechanisms — Script blocking for <script> tags, service adapters for programmatic APIs (PostHog, GA4, etc.). Most real-world setups need both.
  • Adapter pattern — A simple { onConsent, onRevoke } interface. Built-in adapters for 9 popular services, easy to extend with custom adapters. Tools without a consent API (Hotjar, Clarity, etc.) should use script blocking instead.

Browser Support

  • Chrome 67+
  • Firefox 63+
  • Safari 13.1+
  • Edge 79+

(All browsers with Custom Elements v1 + Shadow DOM v1 support)

License

MIT — footage.one