Events & Callbacks
Overview
Zest provides three ways to react to consent changes:
- DOM Events - Standard browser events you can listen to anywhere
Zest.on()/Zest.once()subscribers (v2.0.0+) - Cleaner API with explicit unsubscribe- Callbacks - Functions defined in your configuration
DOM Events
zest:ready
Fired when Zest has initialized and determined the current consent state.
document.addEventListener('zest:ready', (e) => {
console.log('Zest initialized');
console.log('Current consent:', e.detail.consent);
// { essential: true, functional: false, analytics: false, marketing: false }
});
zest:consent
Fired when the user accepts cookies (either all or selected categories).
document.addEventListener('zest:consent', (e) => {
console.log('User consented');
console.log('Categories:', e.detail.consent);
console.log('Previous state:', e.detail.previous);
if (e.detail.consent.analytics) {
initializeAnalytics();
}
if (e.detail.consent.marketing) {
initializeMarketing();
}
});
zest:reject
Fired when the user rejects all non-essential cookies.
document.addEventListener('zest:reject', (e) => {
console.log('User rejected all');
console.log('Consent state:', e.detail.consent);
// { essential: true, functional: false, analytics: false, marketing: false }
});
zest:change
Fired on any consent change (accept, reject, or modification).
document.addEventListener('zest:change', (e) => {
console.log('Consent changed');
console.log('New state:', e.detail.consent);
console.log('Previous state:', e.detail.previous);
// Check what changed
const prev = e.detail.previous;
const curr = e.detail.consent;
if (prev.analytics !== curr.analytics) {
console.log('Analytics consent changed to:', curr.analytics);
}
});
zest:show
Fired when a Zest surface is displayed.
document.addEventListener('zest:show', (e) => {
console.log('UI shown:', e.detail.type);
// 'banner' | 'modal' | 'notice'. 'notice' is the geo "Do Not Sell" link
});
zest:hide
Fired when a Zest surface is hidden. detail.type is 'banner' or 'modal'.
document.addEventListener('zest:hide', (e) => {
console.log('UI hidden:', e.detail.type); // 'banner' or 'modal'
});
zest:geo
Since v2.4.0.
Fired once geo resolution completes (only when geo is configured). The detail carries the chosen action and the sanitized jurisdiction verdict (null if resolution failed and the fallback action was applied).
document.addEventListener('zest:geo', (e) => {
console.log('Action:', e.detail.action);
// 'consent' | 'notice' | 'allow' | 'block'
console.log('Verdict:', e.detail.verdict);
// { isEU, isEEA, isGDPR, isCCPA, isUSPrivacy, regulations: [...] } or null
});
This is the primary hook for the headless build, which renders no UI — show your own banner on 'consent', your own “Do Not Sell” link on 'notice'.
zest:embed
Since v3.0.0.
Fired when a third-party iframe is blocked or activated. In the headless build, use this event to render your own load control; no overlay is supplied. Loading one embed with Zest.activateEmbed() doesn’t record consent.
document.addEventListener('zest:embed', (e) => {
const { phase, element, category, url } = e.detail;
// phase: 'blocked' | 'activated'; element is the iframe
console.log(phase, element, category, url);
});
Zest.on(Zest.EVENTS.EMBED, (e) => console.log(e.detail.phase));
DNT Auto-Reject Fires Your Listeners
With the default dntBehavior: 'reject', a visitor sending DNT or GPC is auto-rejected during initialization, before any UI appears. That reject is a real consent change: zest:reject, zest:change, and the onReject / onChange callbacks all fire, exactly as if the visitor had clicked Reject. If your analytics code initializes off those events, expect it to run (and immediately see everything denied) for these visitors. See DNT, GPC and Geo for how the signal interacts with geo gating.
Subscriber API
Since v2.0.0.
Zest.on() and Zest.once() are thin wrappers around the DOM events above that return an unsubscribe function and let you omit the zest: prefix. Handy when you want a clean teardown path (e.g. inside a SPA component).
Zest.on(event, callback)
const unsubscribe = Zest.on('consent', (consent) => {
if (consent.analytics) initAnalytics();
});
// Later, stop listening
unsubscribe();
The event name accepts both forms:
Zest.on('change', handler); // short form
Zest.on('zest:change', handler); // full event name
Zest.on(Zest.EVENTS.CHANGE, handler); // via constants
Zest.once(event, callback)
Same signature, but the callback fires at most once and then auto-unsubscribes:
Zest.once('ready', (consent) => {
console.log('Zest is ready, current consent:', consent);
});
Unsubscribing everywhere
Every subscriber — DOM event, on, once, callback — receives the same payload shape. Pick the style that fits your codebase; you don’t have to be consistent.
// In a React component, on/off cleanly
useEffect(() => {
const unsubscribe = Zest.on('change', (consent) => {
setConsent(consent);
});
return unsubscribe;
}, []);
Configuration Callbacks
Define callbacks in your ZestConfig for a cleaner setup:
window.ZestConfig = {
callbacks: {
onReady: (consent) => {
console.log('Ready with consent:', consent);
},
onAccept: (consent) => {
console.log('Accepted:', consent);
if (consent.analytics) {
gtag('config', 'GA-XXXXX');
}
},
onReject: () => {
console.log('All rejected');
},
onChange: (consent) => {
console.log('Changed to:', consent);
}
}
};
Callback Reference
| Callback | Parameters | When Fired |
|---|---|---|
onReady |
consent |
Zest initialized |
onAccept |
consent |
User accepted (any categories) |
onReject |
none | User rejected all |
onChange |
consent |
Any consent change |
onGeo |
action, verdict |
Geo resolution completed — only when geo is configured (v2.4.0+) |
Practical Examples
Google Analytics Integration
document.addEventListener('zest:consent', (e) => {
if (e.detail.consent.analytics) {
// Load gtag.js dynamically
const script = document.createElement('script');
script.src = 'https://www.googletagmanager.com/gtag/js?id=GA-XXXXX';
script.async = true;
document.head.appendChild(script);
script.onload = () => {
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA-XXXXX');
};
}
});
Facebook Pixel Integration
document.addEventListener('zest:consent', (e) => {
if (e.detail.consent.marketing) {
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', 'PIXEL_ID');
fbq('track', 'PageView');
}
});
Consent-Aware Analytics Wrapper
class ConsentAwareAnalytics {
constructor() {
this.queue = [];
this.initialized = false;
document.addEventListener('zest:consent', (e) => {
if (e.detail.consent.analytics) {
this.initialize();
}
});
// Check if already consented
if (typeof Zest !== 'undefined' && Zest.hasConsent('analytics')) {
this.initialize();
}
}
initialize() {
if (this.initialized) return;
this.initialized = true;
// Initialize your analytics here
// ...
// Process queued events
this.queue.forEach(event => this.track(event.name, event.data));
this.queue = [];
}
track(name, data) {
if (!this.initialized) {
this.queue.push({ name, data });
return;
}
// Send to analytics
gtag('event', name, data);
}
}
const analytics = new ConsentAwareAnalytics();
// Safe to call anytime - will queue if no consent
analytics.track('button_click', { label: 'signup' });
Show Banner on Footer Link Click
<footer>
<a href="#" onclick="Zest.showSettings(); return false;">
Cookie Preferences
</a>
</footer>
Log Consent for Compliance
document.addEventListener('zest:consent', (e) => {
const proof = Zest.getConsentProof();
// Send to your server
fetch('/api/consent-log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
consent: proof,
userAgent: navigator.userAgent,
url: window.location.href
})
});
});