Wire / Cookie Banners / Article
┌── POST 08.29 · Cookie Banners · 5 min read

tarteaucitron.js and Google Consent Mode v2: Reading the Source, Not the README

tarteaucitron.js ships a built-in googleConsentMode flag. What it actually sets, verified against the library's own source (v1.34.0): default-denied signals, the softConsentMode hard/soft split, and how to wire GA4 and Google Ads through it correctly.

TL;DR

tarteaucitron.js, the MIT-licensed, self-hosted French cookie banner (1,054 GitHub stars, last release v1.34.0 on 2026-07-03), ships a built-in googleConsentMode flag. Turn it on and it sets all four Google Consent Mode v2 signals to denied by default, then updates them as each gated service is accepted or refused. This is a working-code walkthrough of what that flag actually does and how to wire GA4 and Google Ads through it, verified by reading the library’s source rather than its marketing copy.

tarteaucitron.js is one of the more established options in the open-source, self-hosted CMP space — no SaaS dependency, no per-pageview pricing, MIT licence. What is less obvious from a first look at the docs is that it already implements Google Consent Mode v2 as a first-class feature, gated behind a single boolean. This walks through what that boolean turns on, line by line in the actual source, and how to connect it to GA4 and Google Ads.

Turning it on

The relevant flag sits in the standard tarteaucitron.init() call:

tarteaucitron.init({
    "privacyUrl": "/privacy/",
    "cookieName": "tarteaucitron",
    "orientation": "middle",
    "showAlertSmall": false,
    "highPrivacy": true,
    "DenyAllCta": true,
    "AcceptAllCta": true,
    "googleConsentMode": true,
    "softConsentMode": false
});

softConsentMode is worth pausing on, because it changes what “consent mode” means in practice here. It defaults to false in the library itself — meaning the default behaviour is hard blocking: gated scripts stay network-blocked until consent is given, and Consent Mode signals are layered on top of that block rather than replacing it. Set it to true and tarteaucitron switches to signal-only gating for a subset of services (Bing/Clarity, Google Ads, GA4), unblocking the script tags themselves and relying on the gtag('consent', ...) calls to control what Google does with the data. Which behaviour you want depends on whether you are trying to satisfy “no non-essential request before consent” literally, or relying on Google’s own consent-aware request handling.

What googleConsentMode actually sets

With the flag on, tarteaucitron’s init code sets up its own window.dataLayer and a wrapped tac_gtag() push function, independent of whether GA4 or Ads are even configured yet, and immediately declares the default state:

tac_gtag('consent', 'default', {
    ad_storage: 'denied',
    analytics_storage: 'denied',
    ad_user_data: 'denied',
    ad_personalization: 'denied',
    wait_for_update: 800
});

All four signals denied by default, with an 800ms window for the default to apply before any queued tag fires — that is the library’s own choice of value, not something Google mandates. This call has to run before gtag.js loads for Consent Mode to treat it as the default rather than a late update, which is exactly why tarteaucitron issues it during its own init rather than waiting on a service to be added.

From there tarteaucitron listens for its own internal consent events — gtag_consentModeOk / Ko for GA4, googleads_consentModeOk / Ko for Ads, gcmads_consentModeOk / Ko for personalised-ads consent specifically — and pushes matching update calls. Accepting GA4 alone only flips analytics_storage. Accepting Ads flips ad_storage. Personalised ads is tracked as its own consent, separate from plain Ads, and only that one touches ad_user_data and ad_personalization. This split matters if you have ever wondered why a “reject” click sometimes leaves one Consent Mode signal granted — check which of the three events actually fired.

Wiring up GA4

GA4 is a bundled service (tarteaucitron.services.gtag in tarteaucitron.services.js), so adding it is a job push plus one required variable, the measurement ID:

tarteaucitron.user.gtagUa = 'G-XXXXXXXXXX';
(tarteaucitron.job = tarteaucitron.job || []).push('gtag');

The service definition builds its own cookie list dynamically from that ID at runtime — _ga, _gid, _gat_gtag_<id>, _ga_<id> among others — which is what tarteaucitron’s banner displays as the declared cookie list for this service. If you rely on that list for your own transparency copy, it is only as accurate as gtagUa being set correctly; a stale or duplicated measurement ID produces a cookie name that does not match what actually gets set.

Its fallback function is conditional on the same two flags: if googleConsentMode is on and softConsentMode is off, the fallback still runs the script — because in hard-blocking mode, “consent denied” is enforced by the block, not by withholding the tag load once the block itself has been lifted.

Wiring up Google Ads

Ads consent is split into two tracked events for a reason: a site may want conversion tracking (plain Ads, ad_storage) without personalised advertising (ad_user_data / ad_personalization). tarteaucitron creates a second, synthetic service on the fly — gcmads — the moment a googleads_added event fires, specifically to let personalised ads be accepted or refused independently of the base Ads service. If your banner only exposes one “Advertising” toggle, this distinction is invisible to the user even though the underlying signals are separate — worth deciding deliberately rather than by omission.

Checking it actually works

This is a case where reading the source and reading the running page can disagree, so verify both. Open a clean profile, load the site before interacting with the banner, and in the console run:

dataLayer.filter(e => e[0] === 'consent')

The first entry should be the default call with all four signals denied. Accept GA4 only, and a second update entry should show analytics_storage: 'granted' with the other three untouched. If the default call is missing, or arrives after gtag.js has already loaded, the ordering is wrong and Google will not honour it as a default — Consent Mode is order-sensitive in a way that fails silently. We covered the general version of this check, independent of which CMP is doing the setting, in the vanilla-cookieconsent Consent Mode setup, and the GTM-specific timing rules in default vs. update execution order.

Declared behaviour and actual behaviour can still drift after this is wired up correctly once — a plugin adds a new tag, someone edits the GTM container directly, and the banner’s promises stop matching what the page does. Check your Consent Mode signals against what actually fires rather than trusting the config was never touched again.

The short version

tarteaucitron does not require bolting Consent Mode on yourself — it is a documented flag with a real implementation behind it, verified here against v1.34.0’s own source rather than assumed from the README. The two decisions worth making deliberately are softConsentMode (hard block vs. signal-only) and whether your UI exposes personalised-ads consent as distinct from plain Ads, since the library already tracks them separately whether or not your banner shows it. For the wider open-source landscape this sits in, see the open-source CMP comparison.

C
About the author
Consent Mode HQ
Editorial team at Consent Mode HQ
Read more by author ↗