libconsent

How it works

What happens under the hood. Useful for debugging, not required for using libconsent.

Nothing on this page is needed to use libconsent. It is here for when something behaves unexpectedly and you want to know why.

The rule

Whether something may run comes down to four steps, applied identically by the gate, the hooks and the preferences dialog:

If the consent UI has an answer for this specific service — under any of the names it is known by — that answer is used, and that is the end of it.
Otherwise essential is granted, immediately and without waiting.
Otherwise nothing is granted until the platform has reported in.
Otherwise the service inherits its category.

Step one is deliberately first. Whatever the user was shown and clicked is what happens — a service they turned off does not run because its category is on, because the site marked it essential, or because Consent Mode would have let it load. There is no route around it.

It is also why ambiguity resolves to denied. If a platform reports the same service twice and the answers disagree, the denial wins:

// The user allowed one row and refused the other.
services: { "Google Analytics": true, "GA4": false }
// → useHasConsent("google-analytics") === false

Names are compared with case and punctuation removed, so Google Analytics 4, google_analytics_4 and googleAnalytics4 are the same key.

One caveat, for `essential` only

Step two runs before the platform has reported, which is the point of essential — those services are needed for the site to work and don't wait for an answer. If the platform then reports a denial for one, it stops rendering, but it will have rendered until that point. Everything in the other three categories waits, so this only applies to what you marked essential.

Why nothing is granted at first

useConsent().ready is false until the platform answers, and the platform only answers in the browser. So the server render and the first client render agree: gated components show their fallback, then swap once consent is known. There is nothing to suppress and no flash of the wrong thing — as long as you don't read localStorage yourself to get ahead of it.

State

{
  ready,        // the platform has loaded and answered
  hasDecision,  // the user chose, rather than us showing defaults
  view,         // "hidden" | "banner" | "settings"
  categories,   // { essential: true, analytics: false, … }
  services,     // per-service answers, by normalised name
}

view is which consent UI libconsent should render. Platforms that draw their own banner always report "hidden", which is why <ConsentBanner> can stay mounted while you switch between them.

Adapters

An adapter is one function. It loads the platform's script, subscribes to whatever that platform calls a consent event, reports the answer in the shape above, and returns the actions the platform supports:

{ open, openSettings, close, acceptAll, rejectAll, reset, setConsent }

All of them are optional. useConsent().supports reports what a platform can actually do, and calling something it can't warns in development rather than throwing in production.

Reports replace categories and services rather than merging them — a platform sends its whole answer each time, and an override it stops mentioning has been withdrawn. Identical reports are dropped, so a platform re-announcing unchanged consent on every click doesn't cause a render.

Writing an adapter is covered in Writing an adapter.

Categories

The four unified categories and what platforms tend to call them:

libconsentCookiebotOneTrustUsercentricsconsentmanager
essentialnecessaryC0001essentialNecessary
functionalpreferencesC0003functionalFunctional
analyticsstatisticsC0002analyticsMeasurement
marketingmarketingC0004/C0005marketingAdvertising

A platform with categories of its own keeps them: they appear in useConsent().categories under their own name, and useHasConsent("social-media") answers. The four above are the ones you can rely on everywhere.

On this page