Skip to main content

Menu

Choose a theme and configure high-contrast mode. Preferences are saved in your browser only.

User Preferences

Theme

Pick a palette or follow your system preference.

High Contrast

Sharper text and borders. System follows your OS setting.

Headless

Since v2.0.0. Headless API surface complete since v2.8.0.

Overview

The headless entry runs the full consent engine with zero UI. No banner, no settings modal, no widget, no translations, no injected stylesheet, no Shadow DOM. You render the consent surface yourself in whatever framework you already use, and Zest handles the parts that are hard to get right: cookie and storage interception, script blocking, embed gating, DNT/GPC signals, geo resolution, and the consent record itself.

import Zest from '@freshjuice/zest/headless';

const snapshot = Zest.init({ mode: 'safe', policyUrl: '/privacy-policy' });

if (!snapshot.hasDecision && !snapshot.dntApplied) {
  myBanner.show();
}

Key differences from the full build:

Full build Headless
Consent engine, interceptors, script blocking Yes Yes
Banner, modal, widget, “Do Not Sell” notice Yes No
Embed consent overlay Yes No
Zest.activateEmbed() Yes Yes (you render your own load control)
Translations (12 languages) Yes No
Auto-init on load Yes No, you call init() explicitly
window.Zest global Yes No
Bundle format IIFE script tag + ESM ESM only, ~16 KB gzipped

What’s Included

Everything except the UI methods (show, hide, showSettings, hideSettings):

Zest.init(config);          // initialize, returns a snapshot (see below)
Zest.getConsent();          // current categories
Zest.hasConsent('analytics');
Zest.hasConsentDecision();  // any decision recorded?
Zest.getConsentProof();     // signed proof payload for compliance logs
Zest.acceptAll();
Zest.rejectAll();
Zest.updateConsent({ analytics: true, marketing: false });
Zest.reset();               // clears the consent cookie (your UI decides what to do next)
Zest.resolveGeo();          // Promise of { action, verdict } when geo is configured
Zest.isDoNotTrackEnabled();
Zest.getDNTDetails();
Zest.activateEmbed(elementOrIndex);
Zest.on(event, fn);         // returns unsubscribe
Zest.once(event, fn);
Zest.getConfig();
Zest.version;

Named exports are also available if you tree-shake:

import { init, hasConsent, on } from '@freshjuice/zest/headless';

The init() Snapshot

Zest.init() returns a snapshot you branch on. This is the whole wiring pattern for a custom UI:

const s = Zest.init({ geo: true });

if (s.hasDecision) {
  // Returning visitor with a recorded choice. Render nothing.
} else if (s.dntApplied) {
  // Visitor was auto-rejected by a DNT/GPC signal. Render nothing.
} else if (s.geoPending) {
  // Geo resolution in flight. Hold your UI until zest:geo fires,
  // then branch on the action (see the geo docs for headless details).
} else {
  showMyBanner();
}
Field Meaning
hasDecision A consent choice exists in the zest_consent cookie
dntApplied The visitor’s DNT/GPC signal auto-rejected all non-essential categories
geoPending Geo is configured and the jurisdiction verdict is still in flight
consent Current category state
alreadyInitialized init() was called before; this call made no changes

init() is idempotent: interceptors install exactly once, and a second call (a React StrictMode double-mount, for example) just returns the current state.

Wiring Your Own UI

Subscribe to changes, drive your form from the state:

Zest.init({ mode: 'safe', essentialKeys: ['my_app_settings'] });

Zest.on('change', (consent) => {
  syncMyCheckboxes(consent);
});

mySaveButton.addEventListener('click', () => {
  Zest.updateConsent(readMyCheckboxes());
});

Blocked embeds surface through the zest:embed event instead of the built-in overlay, so render your own “load content” control:

Zest.on('embed', (e) => {
  if (e.detail.phase === 'blocked') {
    addMyLoadButton(e.detail.element, () => Zest.activateEmbed(e.detail.element));
  }
});

Next Steps