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)

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; }
    `,
  },
});
TokenDefault (light)What it styles
--cl-bg / --cl-fg / --cl-muted#fff / #18181b / #6b6b76Surfaces & text
--cl-accent / --cl-accent-fg#18181b / #fffPrimary button, switches (via --cl-switch-on)
--cl-btn-bg / --cl-btn-fg#f2f2f4 / #18181bSecondary buttons
--cl-radius / --cl-btn-radius16px / derived (radius Γ— .625)One radius knob scales everything: cards, buttons, category rows (Γ— .75); switch & floating-button pills go square at 0
--cl-pad20pxOne spacing knob scales all card & dialog padding: banner, preferences sections (Γ— .7–.9), category rows, list gaps (Γ— .5)
--cl-width24.5remBox banner width
--cl-border / --cl-border-wrgba(9,9,11,.1) / 0Surface 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-overlaysoftElevation & 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-zsystem / 14px / 2147483000Typography & stacking

Try tokens live in the configurator or the playground.