Headless / custom UI
Designers: the built-in UI is intentionally minimal and fully themeable β but if you want pixel-perfect control, run the engine headless (ui: false) and keep everything else: storage, revisions, gating, auto-clear, Consent Mode, events, adapters.
Engine only
import * as ConsentLoop from "consentloop";
await ConsentLoop.run({
ui: false,
categories: { necessary: { required: true }, analytics: {}, marketing: {} },
googleConsentMode: true,
});
// Decide whether to show *your* banner:
if (!ConsentLoop.hasConsent()) showMyBanner();
// Wire your buttons:
myAcceptButton.onclick = () => { ConsentLoop.acceptAll(); hideMyBanner(); };
myRejectButton.onclick = () => { ConsentLoop.rejectAll(); hideMyBanner(); };
mySaveButton.onclick = () =>
ConsentLoop.accept(["analytics"], { analytics: ["ga4"] }); // granular
π‘Everything still works headless: gated
<script type="text/plain" data-consent> tags activate on accept*(), cookies auto-clear on withdrawal, consentloop:* DOM events fire for your analytics.Custom UI checklist (stay compliant)
- Equal prominence for Accept and Reject under GDPR β no color tricks.
- A way back in: call
showPreferences()-equivalent (your dialog) from a persistent footer link. - No pre-ticked optional boxes (
default: trueis for opt-out regimes only). - Keyboard + screen reader support:
role="dialog", focus trap, Esc. - Re-prompt on policy change by bumping
storage.revision.
Theming instead of replacing
Often you donβt need headless β the built-in UI accepts design tokens and, with ui.shadow: false, plain CSS:
ConsentLoop.run({
ui: {
tokens: {
accent: "#0ea5e9",
"accent-fg": "#fff",
radius: "6px",
font: "'Inter', sans-serif",
shadow: "0 4px 24px rgba(2,132,199,.25)",
width: "22rem",
},
},
});
Need to restyle a single element beyond what tokens cover? ui.customCss injects your CSS inside the Shadow DOM, after the built-in styles β full control without giving up isolation. The .cl-* class names are a stable API:
ConsentLoop.run({
ui: {
customCss: `
.cl-banner { border: 1px solid var(--cl-accent); }
.cl-title { text-transform: uppercase; letter-spacing: .04em; }
`,
},
});
| Token | Default (light) | What it styles |
|---|---|---|
--cl-bg / --cl-fg / --cl-muted | #fff / #18181b / #6b6b76 | Surfaces & text |
--cl-accent / --cl-accent-fg | #18181b / #fff | Primary button, switches (via --cl-switch-on) |
--cl-btn-bg / --cl-btn-fg | #f2f2f4 / #18181b | Secondary buttons |
--cl-radius / --cl-btn-radius | 16px / derived (radius Γ .625) | One radius knob scales everything: cards, buttons, category rows (Γ .75); switch & floating-button pills go square at 0 |
--cl-pad | 20px | One spacing knob scales all card & dialog padding: banner, preferences sections (Γ .7β.9), category rows, list gaps (Γ .5) |
--cl-width | 24.5rem | Box banner width |
--cl-border / --cl-border-w | rgba(9,9,11,.1) / 0 | Surface border color & width (banner, dialog, floating button). Borderless by default β the shadow carries the edge; try 1px for outlined surfaces or 2px + radius 0 for a neobrutalist look. Category rows & internal dividers keep a hairline. |
--cl-shadow / --cl-overlay | soft | Elevation & modal backdrop. Any CSS box-shadow works: none for flat, a tight blur for subtle, a long soft blur for floating β pair none with a visible --cl-border-w so surfaces keep an edge |
--cl-font / --cl-text / --cl-z | system / 14px / 2147483000 | Typography & stacking |
Try tokens live in the configurator or the playground.