Small, script-first consent mode library for Google/GTM aligned consent controls.
- Shows a compact popup with quick
Accept allandReject all. - Provides a
dialog-based preferences UI with category toggles. - Keeps any category marked
required: truealways on. - Defaults to opt-out unless configured otherwise.
- Pushes consent defaults/updates to
dataLayer, and usesgtag('consent', ...)when available. - Gates scripts with
data-consent-categoryviaMutationObserver. - Emits custom consent lifecycle events.
window.ConsentOptions = {
categories: {
necessary: { consent: ['security_storage'], required: true },
marketing: { consent: ['ad_storage', 'ad_user_data', 'ad_personalization'], required: false },
analytics: { consent: ['analytics_storage'], required: false },
preferences: { consent: ['functionality_storage', 'personalization_storage'], required: false },
}
};required: true locks that category on and shows the always-active label in the UI. Multiple categories can be marked required.
Load the library first on page, before tag scripts that should be consent-gated.
<script>
window.ConsentOptions = {
defaultMode: 'opt-out',
version: 1,
styles: '/dist/css/theme-light.css',
className: 'brand-consent',
links: [
{ title: 'Privacy policy', url: '/privacy' },
{ title: 'Cookie policy', url: '/cookies' }
]
};
</script>
<script src="/dist/js/consent.bundled.js"></script>
<script data-consent-category="analytics" src="https://example.com/analytics.js"></script>
<!-- Inline scripts should start inert and be activated by Anubis -->
<script type="text/plain" data-type="text/javascript" data-consent-category="analytics">
console.log('inline analytics runs only after consent allows it');
</script>dist/js/consent.bundled.js auto-injects the base structural CSS.
Use styles as the single customization input for Shadow Root styling:
- Pass a stylesheet URL string, for example
'/dist/css/theme-light.css' - Or pass an inline CSS string block
When styles is a stylesheet URL, the banner stays hidden until that stylesheet finishes loading (or errors), which avoids first-paint unstyled flashes.
Anubis mounts its UI in a Shadow Root by default to isolate consent UI styles from host frameworks (for example Bulma/Tailwind).
The base stylesheet (src/css/base.css) also applies safe typography/control defaults inside the Anubis root (color, font-size, font-weight, line-height, form control inheritance, box sizing) to reduce inherited host-style drift. For visual customization, prefer editing theme files (theme-light / theme-dark) or overriding Anubis CSS variables instead of relying on host-page framework styles.
If you change window.Anubis.getOptions().styles at runtime, call window.Anubis.refreshStyles() (or api.refreshStyles() in ESM mode) to re-apply shadow-root styles.
To clear consent storage quickly during testing, call window.Anubis.reset() (or api.reset() in ESM mode). This clears the mirrored consent cookie/localStorage entry, resets in-memory consent to defaults, and reopens the consent banner.
src/js/*→ runtime, UI, storage, consent-mode, debug helpersrc/css/base.css→ structural-only CSSsrc/css/theme-light.css/src/css/theme-dark.css→ paint/theme layers
Most teams use the global options object before loading dist/js/consent.bundled.js.
window.ConsentOptions = {
autoStart: true,
version: 1,
defaultMode: 'opt-out', // or 'opt-in'
waitForUpdate: 500,
storageDuration: 180,
storageKey: 'consent-options',
links: [
{ title: 'Privacy policy', url: '/privacy' },
{ title: 'Cookie policy', url: '/cookies' },
],
styles: '/dist/css/theme-light.css',
className: 'brand-a-consent',
categories: {
necessary: { consent: ['security_storage'], required: true },
marketing: { consent: ['ad_storage', 'ad_user_data', 'ad_personalization'], required: false },
analytics: { consent: ['analytics_storage'], required: false },
preferences: { consent: ['functionality_storage', 'personalization_storage'], required: false },
},
};links supports any number of banner links (title + url).
Common options:
-
className→ optional custom class (or space-separated classes) added to the shadow-root container element (which already includesroot). -
Core:
autoStart,version,defaultMode,storageDuration,storageKey -
Consent behavior:
defaultConsent,unknownPolicy,reloadOnRevoke,respectDoNotTrack,waitForUpdate,stylesTimeout -
Categories/mapping:
categories,consentMapping -
UI/i18n:
links,actions,localeActive,localeFallback,i18n.locales -
Region:
region,regionResolver,regionTimeout,regionCache,regionKey,regionDuration,regionOverrides
defaultMode baseline behavior: opt-out starts consent keys as granted, while opt-in starts as denied (required categories are still forced to granted).
respectDoNotTrack defaults to true. When enabled and the browser DNT signal is on, Anubis initializes consent as denied (except required categories), stores that state, and replaces the first-run consent banner with a small DNT notice banner.
waitForUpdate maps to Google Consent Mode wait_for_update on the initial gtag('consent', 'default', ...) call.
stylesTimeout controls the maximum wait (in milliseconds) for a linked theme stylesheet before allowing banner display. Default is 0 (no timeout, wait indefinitely). Set a positive value (for example 5000) if you prefer a bounded wait.
For the required-category helper label, localize the global labelRequired UI key.
For per-category state labels in the switch row, localize stateActive and stateDisabled.
For the DNT notice text, localize the global doNotTrackNotice UI key.
For the DNT notice dismiss button label, localize the global buttonCloseNotice UI key.
consentMapping is optional and only needed when your internal consent keys differ from Google Consent Mode keys. If your category consent lists already use Google keys (for example analytics_storage, ad_storage), you can omit consentMapping.
If you are bundling with Vite/Webpack/Rollup, use ESM:
import startAnubis from '/dist/js/consent.esm.js';
startAnubis({
defaultMode: 'opt-out',
version: 1,
});regionResolver lookups are cached by default (regionCache: true) and mirrored to both cookie + localStorage.
actions lets teams control which built-in buttons render in banner/dialog and in what order.
- Base options
- Region override chain (broad to specific, e.g.
USthenUS-CA) - Stored user choice (if
versionmatches)
regionOverrides supports state/province level keys when region (or regionResolver) returns a composite code like US-CA.
Region values are normalized to uppercase with - separators (so us_ca, us-ca, and US-CA are treated the same).
Example (CCPA / CPRA style):
window.ConsentOptions = {
regionResolver: async () => 'US-CA',
regionOverrides: {
US: {
links: [
{ title: 'Privacy policy', url: '/privacy-us' },
],
},
'US-CA': {
defaultMode: 'opt-out',
actions: {
banner: [
{ id: 'reject', label: 'Reject all' },
{ id: 'accept', variant: 'primary', label: 'Accept all' },
{ id: 'open', variant: 'link', label: 'Your Privacy Choices' },
],
},
},
},
};Library selects strings in this order:
localeActive/i18n.localeActivenavigator.languagematchlocaleFallback/i18n.localeFallback- Built-in English strings
Use attributes on custom UI elements:
<button data-consent="open">Privacy settings</button>
<button data-consent="accept">Allow all</button>
<button data-consent="reject">Reject all</button>
<button data-consent="save">Save choices</button>consent:readyconsent:updatedconsent:revoked
document.addEventListener('consent:updated', (event) => {
console.log(event.detail.state, event.detail.googleState);
});Anubis does both:
- Sends Google Consent Mode commands (
gtag('consent', 'default'|'update', ...)) whengtagexists. - Pushes custom
dataLayerevents every time consent default/update is applied.
If gtag is not present, Anubis creates a Google-style fallback shim:
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}Then consent commands still execute through gtag('consent', ...) and land in dataLayer.
On default:
event: 'consent_default'consentCommand: 'default'consentStateobject with consent keys- flattened consent keys at top-level (for GTM variable access)
consentRegion,consentVersion
On update:
event: 'consent_update'consentCommand: 'update'- same payload fields as above
Use GTM Consent Settings to block/fire tags by consent key:
- In GTM, open each tag and configure Consent Settings.
- Require the relevant consent types, for example:
- Ads tags:
ad_storage(+ad_user_data,ad_personalizationwhen needed) - Analytics tags:
analytics_storage - Preference/personalization tags:
functionality_storage/personalization_storage
- Publish container.
This is the preferred approach for consent gating in GTM.
If you need custom logic, use Anubis dataLayer events:
- Create GTM Event Triggers:
consent_defaultconsent_update
- Create Data Layer Variables for keys you care about (for example
analytics_storage,ad_storage). - Add trigger conditions such as:
- fire only when
analytics_storageequalsgranted - block or exception when
ad_storageequalsdenied
marketing→ad_storage,ad_user_data,ad_personalizationanalytics→analytics_storagepreferences→functionality_storage,personalization_storagenecessary→security_storage(always granted)
npm install
npm run buildOutputs:
dist/js/consent.esm.jsdist/js/consent.js(lean IIFE runtime, no structural CSS injection)dist/js/consent.bundled.js(IIFE runtime + structural style injection)dist/js/debugger.js(debug panel helper)dist/css/consent.css(structural CSS only)dist/css/theme-light.css(default paint/theme layer)dist/css/theme-dark.css(dark paint/theme layer)
Include this only in development/debug sessions:
<script src="/dist/js/consent.bundled.js"></script>
<script src="/dist/js/debugger.js"></script>The helper shows a floating bottom-right panel with:
Statetab: current internal consent stateLogtab: consent lifecycle eventsDataLayertab: recentdataLayersnapshot + push args (includinggtag()wrappers)
examples/anubis-options.example.json(full baseline options;opt-outbaseline with one explicit denied override)examples/region-resolver.example.js(resolver + region overrides)examples/us-state-overrides.example.js(US + state-level overrides)examples/opt-out-except-eu-ca.example.js(safe fallback model: default opt-in, opt-out for non-EU/non-CA)
npm install
npm run build
python3 -m http.server 4173Then open http://localhost:4173/demo/.
Demo includes:
- Preset selector (
default,dialog-first,accept-only,opt-out-except-eu-ca) - Theme selector (
none,light,dark) via query param?theme= - Developer triggers and live event/state logging
- In-browser configurator at
demo/configurator.html(left-side options, right-side preview + JSON)
Demo pages (index, preview, configurator) use Bulma CSS from CDN for baseline styling:
https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css
- On revoke, cookie clearing is best effort for first-party readable cookies only.
- Third-party and HttpOnly cookies cannot be reliably cleared by client JS.