Wire / Cookie Banners / Article
┌── POST 08.19 · Cookie Banners · 6 min read

CookieConsent v3 and Google Consent Mode v2: The Parts the Docs Leave Out

Orest Bida's CookieConsent is the most-starred open-source consent library, but its Consent Mode guide never mentions v2 and skips wait_for_update, ads_data_redaction and the call ordering. The complete wiring, plus why npm i cookieconsent gets you the wrong library.

TL;DR

CookieConsent by Orest Bida is the most-starred open-source consent library on GitHub — 5,628 stars, MIT, still maintained. Its official Google Consent Mode guide sets all seven signals correctly, but never says the words “Consent Mode v2” and leaves out the three things that decide whether the integration works at all: where the default call goes, wait_for_update, and the redaction flags. Also worth knowing before you type anything: npm i cookieconsent installs a different, abandoned library.

We covered Klaro and Consent Mode v2 earlier this month. This is the same job for the other library people actually deploy, and the numbers say it is the more popular of the two by some distance.

First, install the right thing

There are two packages with confusingly similar names and they are not the same project.

  • vanilla-cookieconsent — this is Orest Bida’s library. Latest release 3.1.0, published 4 February 2025, MIT. This is the one you want.
  • cookieconsent — a different project: Osano’s Cookie Consent. It is still developed on GitHub, but the npm package under that bare name was last published 23 May 2019, so installing it gets you a six-year-old build of software you probably were not looking for.

The bare name is the intuitive guess and it is the wrong one. Install it deliberately:

npm install vanilla-cookieconsent

Why this library rather than Klaro

Both are legitimate choices and we have written about Klaro favourably. But if maintenance is a criterion — and for a script that sits on every page of your site it should be — the two have diverged:

  • orestbida/cookieconsent: 5,628 stars, MIT, last commit 23 July 2026.
  • kiprotect/klaro: 1,505 stars, last commit 27 March 2025.

Sixteen months without a commit is not abandonment, but it is a signal worth weighing when you are picking something you will not want to replace.

What this library is not

Setting expectations early saves a rewrite later. CookieConsent is a consent interface and state machine. It is not a full CMP in the vendor sense:

  • No IAB TCF string. If you sell programmatic inventory that needs a TC string, this is not your tool.
  • No hosted vendor database, and no cookie scanner. You describe your own categories and services.
  • No server-side consent log. What it stores is a cookie in the user’s browser. If your legal team wants durable records of consent, you build that.

What you get in exchange is a small dependency you control, no per-domain pricing, and no third-party script deciding when your tags fire. That trade is the whole argument of the build versus buy question, and it is a real trade rather than a free win.

The wiring, in the order it has to happen

Step 1 — defaults, before anything else loads

This is the step that breaks most integrations, and it breaks silently. The default call must execute before gtag.js, before Google Tag Manager, and before the consent library itself. Not in the same block. Before.

<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}

  gtag('consent', 'default', {
    ad_storage:            'denied',
    ad_user_data:          'denied',
    ad_personalization:    'denied',
    analytics_storage:     'denied',
    functionality_storage: 'denied',
    personalization_storage:'denied',
    security_storage:      'granted',
    wait_for_update: 500
  });

  gtag('set', 'ads_data_redaction', true);
  gtag('set', 'url_passthrough', true);
</script>

Three of those lines are the ones the official documentation does not mention.

wait_for_update tells Google’s tags to hold for a given number of milliseconds before acting on defaults, which gives your library time to restore a returning visitor’s stored choice. Without it, a returning user who consented last week gets one page view measured as denied on every load, because the tag fires before the library has read its own cookie.

ads_data_redaction strips ad identifiers from outbound requests while ad_storage is denied. url_passthrough preserves click identifiers across pages in the URL when cookies are unavailable, which is what keeps attribution partially alive in the denied state. Together they are most of the practical difference between “we set the signals” and “Consent Mode is doing something for us.”

Step 2 — map categories to signals

One function, called from every callback, so there is exactly one place where the mapping lives:

function updateGtagConsent() {
  const analytics = CookieConsent.acceptedCategory('analytics');
  const ads       = CookieConsent.acceptedCategory('ads');

  gtag('consent', 'update', {
    analytics_storage:      analytics ? 'granted' : 'denied',
    ad_storage:             ads ? 'granted' : 'denied',
    ad_user_data:           ads ? 'granted' : 'denied',
    ad_personalization:     ads ? 'granted' : 'denied',
    functionality_storage:  analytics ? 'granted' : 'denied',
    personalization_storage:analytics ? 'granted' : 'denied'
  });
}

acceptedCategory(name) returns a boolean, so the mapping stays readable. Note that security_storage is not in the update call — it was granted in the defaults and stays there.

Step 3 — fire it from all three callbacks

CookieConsent.run({
  categories: {
    necessary: { enabled: true, readOnly: true },
    analytics: {},
    ads: {}
  },

  onFirstConsent: updateGtagConsent,
  onConsent:      updateGtagConsent,
  onChange:       updateGtagConsent,

  language: { /* your translations */ }
});

All three matter, and for different reasons. onFirstConsent fires once, when the visitor first chooses. onConsent fires on that first choice and on every subsequent page load — this is the one that restores a returning visitor’s state, and the one people forget. onChange fires when someone edits their preferences later, including a withdrawal.

Wire only onFirstConsent and returning visitors silently run denied forever, while your banner correctly stays hidden. It is a nasty failure because the interface looks right.

How to verify it, in four minutes

  1. Open the site in a clean profile with Tag Assistant recording. On the first hit, before you touch the banner, every signal except security_storage should read denied.
  2. Accept analytics only. analytics_storage flips to granted; all three ad signals stay denied. If ad_user_data moved, your mapping is wrong.
  3. Reload the page. The signals must come back granted without the banner appearing — that is onConsent doing its job.
  4. Withdraw consent from the preferences modal, then watch the network tab. Requests should continue, redacted, not stop entirely. Consent Mode denied is not the same as the tag being removed.

What you still own after this

Consent Mode governs Google’s own tags. It does nothing to the rest of your stack. A Meta pixel, a Hotjar snippet, a TikTok tag or a chat widget pasted into the theme two years ago will keep firing on page load no matter how carefully these seven signals are set — and pre-consent firing by a non-Google tag is the most common finding in every audit we run.

That is not a criticism of this library. It is the boundary of what any consent tool can do for you, and the reason CMP configuration and real-world tracking behaviour keep diverging.

Once the wiring is in, verify it from the outside. Check what your site actually loads before consent with CookieInspector — including everything Consent Mode never touches.

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