libconsent
Adapters

Writing an adapter

Load a script, subscribe to an event, report what the platform says.

An adapter is a connect function. defineAdapter turns it into a typed factory:

import { defineAdapter } from "@libconsent/core";

export const acmeCmp = defineAdapter<{ siteId: string }>({
  id: "acme-cmp",
  name: "Acme CMP",

  async connect({ siteId }, ctx) {
    await ctx.loadScript(`https://cdn.acme-cmp.com/${siteId}.js`);
    const api = await ctx.waitFor(() => window.AcmeCmp);

    const publish = () =>
      ctx.update({
        ready: true,
        hasDecision: api.hasAnswered(),
        categories: {
          essential: true,
          analytics: api.allows("stats"),
          marketing: api.allows("ads"),
        },
      });

    ctx.on("acme:consent", publish);
    publish();

    return {
      open: () => api.showBanner(),
      openSettings: () => api.showSettings(),
      acceptAll: () => api.acceptAll(),
      rejectAll: () => api.rejectAll(),
    };
  },
});
<ConsentProvider adapter={acmeCmp({ siteId: "abc" })}>

The context

Everything the context gives you is tied to the connection: listeners are removed and pending waits reject when the provider unmounts or the adapter changes, so most adapters need no teardown code of their own.

Method
update(patch)Report what the platform says.
getState()What the core currently believes.
loadScript(src, attrs)Inject a <script> once per URL, however often connect runs.
waitFor(getter, opts)Resolve once the getter returns something — for globals that appear late.
on(type, handler, target)addEventListener that cleans itself up.
onDisconnect(fn)Anything else that needs unwinding.
signalAn AbortSignal for your own async work.
warn(message)Namespaced, and silent in production.

What to report

ctx.update({
  ready: true, // you have real values — nothing is granted before this
  hasDecision: false, // the user chose, rather than you reporting defaults
  view: "hidden", // see below
  categories: { essential: true, analytics: false },
  services: { "Google Analytics": true },
});

Service keys are normalised on the way in, so push whatever spelling the platform uses — "Google Analytics", "google_analytics" and "C0002" all become lookup keys as-is.

Fields you leave out keep their current value, but categories and services are replaced rather than merged. Report the whole picture each time: an override you stop mentioning has been withdrawn, and merging would leave a service switched off forever after the user pressed "accept all".

Identical reports are dropped, so a platform that re-announces unchanged consent on every interaction doesn't cause a render each time.

view

view is which consent UI libconsent should render. A platform that draws its own banner should leave it at "hidden", which is what lets <ConsentBanner> stay mounted without rendering anything.

Set it to "banner" or "settings" only if your adapter expects the app to render the UI, as the built-in one does.

Commands

Return the actions your platform supports. Everything is optional; supports reports what you returned, and calling something you didn't warns in development instead of throwing.

return {
  open,
  openSettings,
  close,
  acceptAll,
  rejectAll,
  reset,
  setConsent, // only if the platform lets you write an exact state
  destroy, // anything beyond the context helpers
};

Declaring setConsent enables useConsentForm(), and with it the shadcn banner. Add supportsServices: true if setConsent also honours per-service overrides; without it the dialog offers categories only, rather than switches that would do nothing.

Two flags on the definition

defineAdapter({
  id: "acme-cmp",
  name: "Acme CMP",
  handlesConsentMode: true, // the platform writes Google Consent Mode itself
  supportsServices: true, // setConsent honours per-service overrides
  connect,
});

Idempotence

connect can run more than once: React Strict Mode double-invokes effects in development, and a remounted provider reconnects. loadScript dedupes by URL for you. If you patch a global, restore the old value in onDisconnect, as the OneTrust adapter does for OptanonWrapper.

On this page