Privacy

Matomo with CookieConsent: connecting Orest Bida's consent banner

CookieConsent by Orest Bida is a free consent banner you host yourself, without any third-party service. Here is how to connect it with Matomo so that Matomo always measures cookieless and only sets cookies after consent, what that looks like with Matomo Tag Manager and how the banner also controls scripts from Google or Meta.

A consent banner does not have to come from a cloud provider. CookieConsent by Orest Bida is a small MIT-licensed JavaScript library that you ship with your website. It asks about categories such as statistics and marketing, remembers the decision, releases scripts only after consent and deletes cookies again when someone declines. Matomo hooks into it through callbacks.

Matomo measures every visit without cookies and without browser feature detection. If someone accepts the statistics category, Matomo sets cookies from then on and recognises the visitor. If they decline or withdraw consent, Matomo deletes the cookies. The basic numbers stay complete, and whoever agrees is recognised across several visits. What is missing without cookies is covered in Matomo without cookies.

One banner, three outcomes: before the decision, Matomo measures cookieless and ad scripts stay blocked. After consent, Matomo sets cookies and marketing tags load; after rejection, everything stays cookieless and blocked.

Adding it, self-hosted

CookieConsent is available as the npm package vanilla-cookieconsent, in November 2026 at version 3.3. From its dist folder you need two files, cookieconsent.umd.js and cookieconsent.css, plus a file with your texts. Put all three on your server; the examples in the documentation load them from a CDN, which is not necessary.

<link rel="stylesheet" href="/assets/cookieconsent/cookieconsent.css">
<script src="/assets/cookieconsent/cookieconsent.umd.js"></script>
<script>
  CookieConsent.run({
    categories: {
      necessary: { enabled: true, readOnly: true },
      analytics: {
        // delete Matomo cookies when statistics is declined
        autoClear: { cookies: [{ name: /^_pk_/ }, { name: 'mtm_cookie_consent' }] }
      },
      marketing: {}
    },
    language: {
      default: 'en',
      translations: { en: '/assets/cookieconsent/en.json' }
    },
    onConsent: syncConsent,   // on the first decision and on every page after
    onChange: syncConsent     // when someone changes their selection
  });
</script>

A few CookieConsent defaults already fit: the mode is opt-in, so scripts only run after consent. The accept and reject buttons carry equal weight. And the decision is kept for 182 days in the cc_cookie cookie. With revision, the banner asks again when you change services or categories.

Matomo without Tag Manager

The tracking code gets one extra line, requireCookieConsent. With it, Matomo measures from the start, but without cookies:

<script>
  var _paq = window._paq = window._paq || [];
  _paq.push(['requireCookieConsent']);   // cookieless until consent arrives
  _paq.push(['trackPageView']);
  _paq.push(['enableLinkTracking']);
  (function() {
    var u = 'https://matomo.example.com/';
    _paq.push(['setTrackerUrl', u + 'matomo.php']);
    _paq.push(['setSiteId', '1']);
    var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
    g.async = true; g.src = u + 'matomo.js'; s.parentNode.insertBefore(g, s);
  })();
</script>

The callback that CookieConsent calls above translates the decision into Matomo commands. Define it before CookieConsent.run:

<script>
  function syncConsent() {
    window._paq = window._paq || [];
    if (CookieConsent.acceptedCategory('analytics')) {
      _paq.push(['rememberCookieConsentGiven']);   // cookies allowed from now on
    } else {
      _paq.push(['forgetCookieConsentGiven']);     // delete cookies, back to cookieless
    }
  }
</script>

rememberCookieConsentGiven stores consent in the mtm_cookie_consent cookie; forgetCookieConsentGiven deletes it and the Matomo cookies. Commands via _paq reach every Matomo tracker on the page, including the one from a Tag Manager container.

With Matomo Tag Manager

With the Tag Manager, the callback does not control Matomo directly but sends the decision to the data layer. What happens next is set in the container, for ad tags too:

<script>
  function syncConsent() {
    window._mtm = window._mtm || [];
    window._mtm.push({
      event: 'cc_consent',
      cc_analytics: CookieConsent.acceptedCategory('analytics') ? 'granted' : 'denied',
      cc_marketing: CookieConsent.acceptedCategory('marketing') ? 'granted' : 'denied'
    });
  }
</script>

In the container you need:

What Type Setting
Matomo Configuration variable Require cookie consent on
cc_analytics, cc_marketing Data-Layer variable Data Layer Variable Name cc_analytics or cc_marketing
Consent statistics Custom Event trigger event cc_consent, condition cc_analytics equals granted
Rejection statistics Custom Event trigger event cc_consent, condition cc_analytics equals denied
Consent marketing Custom Event trigger event cc_consent, condition cc_marketing equals granted
Matomo Analytics tag on Pageview as before, measures cookieless until consent
Allow Matomo cookies Custom HTML tag _paq.push(['rememberCookieConsentGiven']), trigger Consent statistics
Remove Matomo cookies Custom HTML tag _paq.push(['forgetCookieConsentGiven']), trigger Rejection statistics
Ad tags for example Facebook Pixel or Google Tag trigger Consent marketing instead of Pageview

In both Custom HTML tags, window._paq = window._paq || []; comes before the command so it also works while Matomo is still loading.

Because CookieConsent calls onConsent on every page where a decision exists, the cc_consent event arrives on every page, and the ad tags fire there, just only after consent. How variables, triggers and versions work in the Tag Manager is covered in Matomo Tag Manager. After every change, publish a new version of the container.

Controlling scripts from Google, Meta and others

Ad scripts may only load after consent. There are three ways to do this:

Through the script tags. CookieConsent only releases a script once its category is accepted if you mark it with type="text/plain" and data-category. With data-service, it gets its own toggle in the banner:

<script type="text/plain" data-category="marketing" data-service="Meta Pixel">
  /* Meta Pixel code */
</script>
<script type="text/plain" data-category="marketing" data-service="Google Ads"
        data-src="https://www.googletagmanager.com/gtag/js?id=AW-XXXXXXX"></script>

Through the Tag Manager. If you maintain ad tags in Matomo Tag Manager, attach them to the Consent marketing trigger, as in the table above.

Through Google’s Consent Mode. Google tags have their own consent signals. They start as denied and are set to granted in the callback:

<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'
  });
  // in the callback, when marketing is accepted:
  // gtag('consent', 'update', { ad_storage: 'granted', ad_user_data: 'granted', ad_personalization: 'granted' });
</script>

How many pixels you need at all is a separate question: conversions can also be reported to ad platforms from the server, described in Conversions API.

Check in the browser

A banner is only as good as what the browser actually does. Open the website in a private window with the developer tools:

  1. Before the decision: under Storage or Application there are no cookies starting with _pk_. In the network tab, requests go to matomo.php, but none to Facebook, Google or other ad networks.
  2. After accepting: the cookies _pk_id, _pk_ses and mtm_cookie_consent appear, and the ad scripts load.
  3. After rejecting or withdrawing: the Matomo cookies are gone, Matomo keeps measuring cookieless, and ad scripts do not load.

Whether your website already sends data to third parties before the decision is also shown by the free website check.

How the banner fits Matomo

  • Cookieless first Matomo always measures, cookies only after consent.
  • All self-hosted Script, styles and texts on your own server.
  • Reject on equal terms Equally weighted buttons, nothing preselected.
  • One callback for everything Matomo, Tag Manager and ad scripts follow the same decision.

Typical pitfalls

  • Matomo blocked instead of cookieless Without consent, all numbers are missing.
  • Pixels directly in the template They load before anyone is asked.
  • No sync on withdrawal The cookies remain although someone declined.
  • Never checked in the browser Banner and reality differ.

Sources

As of 13 November 2026, CookieConsent 3.3, Matomo 5.14 and 6.0.0-rc1

FAQ

Frequently asked questions

Does CookieConsent cost anything?

No. CookieConsent is MIT-licensed and free. You take the files from the npm package vanilla-cookieconsent and put them on your own server.

Does Matomo not measure at all without consent?

That is your decision. With requireCookieConsent, Matomo measures cookieless and without browser feature detection until consent, and with cookies after it. If you do not want to measure at all without consent, use requireConsent instead. The legal assessment stays with your privacy advisers.

Do I need Matomo Tag Manager for this?

No. Without Tag Manager, CookieConsent's callback calls the Matomo commands directly. With Tag Manager, it sends an event to the data layer, and triggers in the container decide which tags fire.

How does the banner ask again after a change?

Through the revision option. If you increase the number, the previous decision is no longer valid and the banner appears again. This makes sense when new services or categories are added.

Questions about your setup?

In an intro call we'll look at your situation together.

Book an intro call

Newsletter

Matomo know-how, once a month.

Practical tips on tracking, privacy settings and performance, plus notes on important Matomo updates. No spam, unsubscribe any time.