# ConsentLoop — complete documentation (single file for LLMs) # Version 0.1.0 · FSL-1.1-MIT (fair source; converts to MIT after 2 years) · npm: consentloop, @consentloop/react # Repo: https://github.com/kocsmy/ConsentLoop ConsentLoop is a source-available cookie consent / consent-management layer built for performance: - ~1.5 KB gzip smart loader on the critical path; full widget (~12.1 KB gzip incl. UI+CSS+logic) loads lazily only when a banner must render. - Zero dependencies, zero third-party network calls, zero layout shift (all surfaces are position:fixed overlays), ~0 main-thread blocking (renders after first paint via requestIdleCallback). - Google Consent Mode v2, GDPR/CCPA presets, per-service consent, cookie auto-clear, i18n with lazy loading + RTL, Shadow-DOM isolation, full a11y, headless mode, React bindings, pluggable backend adapter. ===================================================================== 1. INSTALL ===================================================================== npm: npm i consentloop (core, framework-agnostic) React bindings: npm i consentloop @consentloop/react CDN (best perf): Files shipped in the consentloop package: dist/index.js ESM entry (code-split; UI in a lazy chunk) dist/index.min.js single-file minified ESM dist/consentloop.iife.min.js classic build -> window.ConsentLoop (+ data-auto init) dist/consentloop.loader.min.js ~1.5 KB smart loader (see §9) dist/index.d.ts TypeScript types (JSDoc'd) ===================================================================== 2. QUICK START ===================================================================== ESM: import * as ConsentLoop from "consentloop"; ConsentLoop.run({ categories: { necessary: { required: true }, // auto-added if omitted analytics: { services: { ga4: { label: "Google Analytics 4" } }, autoClear: [/^_ga/, "_gid"], // cookies wiped on withdrawal }, marketing: {}, }, googleConsentMode: true, }); Declarative HTML (no build): Gate scripts (the compliance mechanic): Optional service scoping: data-consent-service="ga4". Activated elements get data-consent-activated and are never touched twice. For SPA-injected elements call ConsentLoop.rescan(). ===================================================================== 3. CONFIGURATION (ConsentLoopConfig) ===================================================================== All keys optional. JSON-schema: https://consentloop.dev/consentloop.schema.json categories: Record void // fires on every load while accepted (boot SDKs here) onReject?: () => void // fires on actual withdrawal cookies?: matchers[] // cleared when this service is rejected }> }> ui: false | { // false = headless (engine only) layout?: "box" | "cloud" | "bar" // default "box" position?: "top-left"|"top-center"|"top-right"|"middle-center"| "bottom-left"|"bottom-center"|"bottom-right" // default bottom-right (box) / bottom-center preferences?: "modal" | "drawer" // default "modal" theme?: "light" | "dark" | "auto" // default "auto" tokens?: Record // CSS vars w/o --cl- prefix, e.g. {accent:"#0ea5e9", radius:"8px", // pad:"16px" (density), border:"#18181b", "border-w":"2px", // font:"Georgia, serif", text:"15px", width:"26rem"} showRejectAll?: boolean // default true (keep true for GDPR) showPreferences?: boolean // default true floatingButton?: boolean // reopen bubble after consent, default false branding?: boolean // small "Powered by ConsentLoop" link (banner + preferences), default true scrollLock?: boolean // lock page scroll while banner/preferences open, default false trapFocus?: boolean // trap keyboard focus in the preferences dialog, default true (WCAG) customCss?: string // extra CSS injected inside the (shadow) root; target .cl-* classes shadow?: boolean // Shadow-DOM isolation, default true container?: string | HTMLElement disableTransitions?: boolean zIndex?: number // default 2147483000 } content: { lang?: string // force language autoDetect?: "document" | "browser" | false // default "document" () fallback?: string // default "en" privacyPolicyUrl?: string // renders localized "Privacy policy" link (banner + preferences) cookiePolicyUrl?: string // renders localized "Cookie policy" link next to it termsUrl?: string // renders localized "Terms & conditions" link next to it translations?: Record // string = URL fetched lazily } Translation = { rtl?: boolean banner?: { title, description /*HTML ok*/, acceptAll, rejectAll, preferences, privacyPolicy?, cookiePolicy?, terms?, // labels for the URL links; pre-translated in all 58 packs links?: [{label, href}] } preferences?: { title, description?, acceptAll, rejectAll, save, close?, alwaysOn?, servicesLabel? } categories?: Record } Missing keys fall back to built-in English. "de-AT" matches "de". 58 ready-made locale packs ship with the package: consentloop/locales/.json (locales/index.json lists codes + names + rtl). Use them as lazy URLs: translations: { de: "https://cdn.jsdelivr.net/npm/consentloop/locales/de.json" } storage: { name?: string // default "cl_consent" type?: "cookie" | "localStorage" // default cookie (server-readable) expiresDays?: number // default 182 domain?, path?, sameSite? // cookie attrs (default path "/", SameSite Lax, Secure on https) revision?: number // default 0; bump to re-prompt everyone } regulation: "gdpr" | "us-optout" | "none" // default "gdpr" gdpr: opt-in; nothing optional runs pre-consent; not dismissable; new optional categories re-prompt. us-optout: category defaults apply immediately as implied consent (method:"implied"); banner offers opting out; Esc persists the implied choice. CCPA/CPRA-style. none: opt-in UI, dismissable without choice. respectGPC: boolean // default true Under us-optout, a visitor sending the Global Privacy Control signal (navigator.globalPrivacyControl) is treated as already opted out: optional categories are NOT granted and no banner is shown (the floating button / showPreferences() still work). Set false to ignore the signal. googleConsentMode: boolean | { map?: Record, waitForUpdate?: number /*500*/ } Default map: necessary→security_storage(always granted); functionality→functionality_storage, personalization_storage; analytics→analytics_storage; marketing→ad_storage,ad_user_data,ad_personalization. Behavior: gtag('consent','default',{all denied except security, wait_for_update}) at run() (stored consent baked in for returning visitors), then gtag('consent','update',…) after every choice. Load ConsentLoop BEFORE gtag/GTM in the document. autoScripts: boolean // default true — scan & activate [data-consent] elements hooks: { onFirstConsent?(record) // first explicit choice onConsent?(record) // every load once consent known + after each change onChange?(record, changedCategories) onBannerShow/Hide?(), onPreferencesShow/Hide?() } adapter: { // custom consent backend; everything fail-open, never blocks UI init?({config, stored}) -> { show?: boolean, regulation?, lang? } | Promise // show:false = no banner needed in this jurisdiction; defaults applied as implied consent persist?(record, ctx) // fire-and-forget consent receipt after every choice } manage: { endpoint, siteId?, timeout? /*750ms*/ } // built-in HTTP adapter (ConsentLoop Cloud): GET {endpoint}/decision?site=… -> RemoteDecision POST {endpoint}/consent (sendBeacon/keepalive; body {site, record}) debug: boolean // default: true on localhost — warns on unknown config keys and // data-consent categories that aren't configured ===================================================================== 4. API (window.ConsentLoop / ESM named exports) ===================================================================== run(config?) -> Promise idempotent; SSR no-op show() / hide() first layer showPreferences() / hidePreferences() acceptAll() / rejectAll() accept(categories: string[], services?: Record) // required cats always included isAccepted(cat) -> boolean // includes implied consent isServiceAccepted(cat, svc) -> boolean acceptedCategories() -> string[] getConsent() -> ConsentRecord | null hasConsent() -> boolean // explicit stored, unexpired, current revision setLanguage(lang) -> Promise // live re-render rescan() // activate newly-added gated elements reset(reprompt = true) // erase consent (+ new id), optionally re-show banner on(event, cb) -> unsubscribe // events: ready, consent, first-consent, change, // banner-show/hide, preferences-show/hide destroy() // teardown (tests/SPA) version DOM events on window mirror on(): "consentloop:consent" (detail = record), etc. ConsentRecord = { id, firstAt, updatedAt, // ISO timestamps; id rotates on reset() revision, accepted: string[], rejected: string[], services: Record, regulation, method: "explicit" | "implied", v } Cookie storage uses short keys: {id,t,u,r,a,j,s,g,m,v} — a=accepted, j=rejected. Server-side: parse cl_consent cookie, check .a.includes("analytics"). ===================================================================== 5. REACT (@consentloop/react) ===================================================================== Runs core once in an effect. SSR-safe, strict-mode safe. No React in widget render path. useConsent() -> { consent, // ConsentRecord|null, re-renders on change (useSyncExternalStore) hasConsent, isAccepted(cat), isServiceAccepted(cat,svc), acceptedCategories(), accept(cats, services?), acceptAll(), rejectAll(), show(), showPreferences(), reset() } }>