Mike Slinn

Free Cookie Consent Library for Microsoft Clarity

Published 2025-09-25.
Time to read: 4 minutes.

This page is part of the posts collection.

This article is a follow-on from Microsoft Clarity Lets Me Watch You Click and Scroll.

I received the following email from Microsoft Clarity for each of my domains.

EMail From Microsoft Clarity

Subject:
Cookie Consent Requirements – updated timeline & action
From:
Microsoft Clarity <maccount@microsoft.com>
Date:
2025-08-27, 8:33 AM
To:
mslinn@mslinn.com
Hi Mike Slinn,

Microsoft Clarity will begin enforcing cookie consent requirements in the European Economic Area (EEA), UK, and Switzerland. To avoid impact to data collection and Clarity features' functionalities, you must send an explicit consent signal to Clarity using one of the supported methods. We are rolling out enforcement in phases, with full enforcement taking effect on October 31st, 2025.

We have received numerous questions regarding the upcoming changes to cookie consent requirements, and we would like to help clarify what this means for you.

What Does This Mean for mslinn.com?

Action Required:

Starting October 31st, you are required to share user consent signals for sessions originating from the EEA, UK, and Switzerland. If consent is not obtained and signaled using one of the supported methods, certain Clarity features will be impacted. Please ensure consent signals are implemented before this date to avoid disruptions to mslinn.com functionality for users in these regions. For more details, refer to our Clarity Consent documentation.

Rollout Status: This requirement will be enforced across the European region by October 31st, 2025.

FAQs

  • What happens if consent is not provided?
    Without explicit consent, features such as session recordings and funnel tracking may be impacted. Data collected during these sessions will not be associated to a visitor without a valid cookie.
  • If I don't obtain cookie consent, what impact does it have on my end users?
    End users will not experience any impact to their interaction with the website. However, your experience with the Clarity site will be impacted, as the functionality of specific Clarity features will be limited.
  • How do I implement the consent API?
    Please refer to the Clarity Consent API documentation for detailed guidance on how to call the API.
  • Will my existing Clarity setup need changes?
    Yes, if you're not currently sending consent signals. You may need to:
  • How can a site owner verify that they implemented the cookie consent correctly on their website?
    Refer to the Consent Mode documentation to verify that cookie consent has been implemented correctly. For more details, refer to our Consent documentation and Consent FAQs.
  • How do I implement consent for mobile app projects?
    No action is currently required for mobile app projects in Clarity.
If you have any questions or need assistance, please don’t hesitate to contact our support team at clarityms@microsoft.com.
Thank you,
The Microsoft Clarity Team
 – Email from Microsoft Clarity

Possible Solutions

This article discusses the simplest free solution that meets Clarity’s requirements for GDPR compliance.

For advanced features (e.g., category-based consent), you would need a more complex script or a consent management platform like CookieYes, which is not free.

If your sites use a content management system, like WordPress, you can use a free plugin like Complianz (free tier) to manage the banner and call the Clarity API.

Simplest Free Solution

The following image is taken from a Clarity recording of a an anonymous Norweigan user session that uses the solution presented in this article. The orange trail shows the path of the user's mouse.

Stylistic changes can be made by editing cookie-consent-script.js, shown below.

To meet Microsoft Clarity’s cookie consent requirements without paying for a third-party cookie management platform (CMP) like CookieYes or relying on Google Tag Manager, you can use a custom JavaScript solution with the Clarity Consent API and a free, open-source consent banner library like CookieConsent. This approach is straightforward, and merely requires adding a consent banner, and calling Clarity’s Consent API to signal user consent for EEA/UK/Switzerland.

The CookieConsent library stores user consent on their browser for up to a year.

Implementation

Below is the step-by-step implementation guide. Excellent documentation is also available here.

Three script Tags

I had previously installed the Clarity JavaScript. Cookie consent requires two additional bits of JavaScript:

  1. The cookieconsent library (formats to 600 lines of JavaScript):
    Formatted cookieconsent.min.js
    ! function(e) {
        if (!e.hasInitialised) {
            var t = {
                escapeRegExp: function(e) {
                    return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&")
                },
                hasClass: function(e, t) {
                    var i = " ";
                    return 1 === e.nodeType && (i + e.className + i).replace(/[\n\t]/g, i).indexOf(i + t + i) >= 0
                },
                addClass: function(e, t) {
                    e.className += " " + t
                },
                removeClass: function(e, t) {
                    var i = new RegExp("\\b" + this.escapeRegExp(t) + "\\b");
                    e.className = e.className.replace(i, "")
                },
                interpolateString: function(e, t) {
                    var i = /{{([a-z][a-z0-9\-_]*)}}/gi;
                    return e.replace(i, function(e) {
                        return t(arguments[1]) || ""
                    })
                },
                getCookie: function(e) {
                    var t = "; " + document.cookie,
                        i = t.split("; " + e + "=");
                    return i.length < 2 ? void 0 : i.pop().split(";").shift()
                },
                setCookie: function(e, t, i, n, o, s) {
                    var r = new Date;
                    r.setDate(r.getDate() + (i || 365));
                    var a = [e + "=" + t, "expires=" + r.toUTCString(), "path=" + (o || "/")];
                    n && a.push("domain=" + n), s && a.push("secure"), document.cookie = a.join(";")
                },
                deepExtend: function(e, t) {
                    for (var i in t) t.hasOwnProperty(i) && (i in e && this.isPlainObject(e[i]) && this.isPlainObject(t[i]) ? this.deepExtend(e[i], t[i]) : e[i] = t[i]);
                    return e
                },
                throttle: function(e, t) {
                    var i = !1;
                    return function() {
                        i || (e.apply(this, arguments), i = !0, setTimeout(function() {
                            i = !1
                        }, t))
                    }
                },
                hash: function(e) {
                    var t, i, n, o = 0;
                    if (0 === e.length) return o;
                    for (t = 0, n = e.length; t < n; ++t) i = e.charCodeAt(t), o = (o << 5) - o + i, o |= 0;
                    return o
                },
                normaliseHex: function(e) {
                    return "#" == e[0] && (e = e.substr(1)), 3 == e.length && (e = e[0] + e[0] + e[1] + e[1] + e[2] + e[2]), e
                },
                getContrast: function(e) {
                    e = this.normaliseHex(e);
                    var t = parseInt(e.substr(0, 2), 16),
                        i = parseInt(e.substr(2, 2), 16),
                        n = parseInt(e.substr(4, 2), 16),
                        o = (299 * t + 587 * i + 114 * n) / 1e3;
                    return o >= 128 ? "#000" : "#fff"
                },
                getLuminance: function(e) {
                    var t = parseInt(this.normaliseHex(e), 16),
                        i = 38,
                        n = (t >> 16) + i,
                        o = (t >> 8 & 255) + i,
                        s = (255 & t) + i,
                        r = (16777216 + 65536 * (n < 255 ? n < 1 ? 0 : n : 255) + 256 * (o < 255 ? o < 1 ? 0 : o : 255) + (s < 255 ? s < 1 ? 0 : s : 255)).toString(16).slice(1);
                    return "#" + r
                },
                isMobile: function() {
                    return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
                },
                isPlainObject: function(e) {
                    return "object" == typeof e && null !== e && e.constructor == Object
                },
                traverseDOMPath: function(e, i) {
                    return e && e.parentNode ? t.hasClass(e, i) ? e : this.traverseDOMPath(e.parentNode, i) : null
                }
            };
            e.status = {
                deny: "deny",
                allow: "allow",
                dismiss: "dismiss"
            }, e.transitionEnd = function() {
                var e = document.createElement("div"),
                    t = {
                        t: "transitionend",
                        OT: "oTransitionEnd",
                        msT: "MSTransitionEnd",
                        MozT: "transitionend",
                        WebkitT: "webkitTransitionEnd"
                    };
                for (var i in t)
                    if (t.hasOwnProperty(i) && "undefined" != typeof e.style[i + "ransition"]) return t[i];
                return ""
            }(), e.hasTransition = !!e.transitionEnd;
            var i = Object.keys(e.status).map(t.escapeRegExp);
            e.customStyles = {}, e.Popup = function() {
                function n() {
                    this.initialise.apply(this, arguments)
                }
                 function o(e) {
                    this.openingTimeout = null, t.removeClass(e, "cc-invisible")
                }
                 function s(t) {
                    t.style.display = "none", t.removeEventListener(e.transitionEnd, this.afterTransition), this.afterTransition = null
                }
                 function r() {
                    var t = this.options.onInitialise.bind(this);
                    if (!window.navigator.cookieEnabled) return t(e.status.deny), !0;
                    if (window.CookiesOK || window.navigator.CookiesOK) return t(e.status.allow), !0;
                    var i = Object.keys(e.status),
                        n = this.getStatus(),
                        o = i.indexOf(n) >= 0;
                    return o && t(n), o
                }
                 function a() {
                    var e = this.options.position.split("-"),
                        t = [];
                    return e.forEach(function(e) {
                        t.push("cc-" + e)
                    }), t
                }
                 function c() {
                    var e = this.options,
                        i = "top" == e.position || "bottom" == e.position ? "banner" : "floating";
                    t.isMobile() && (i = "floating");
                    var n = ["cc-" + i, "cc-type-" + e.type, "cc-theme-" + e.theme];
                    e["static"] && n.push("cc-static"), n.push.apply(n, a.call(this));
                    p.call(this, this.options.palette);
                    return this.customStyleSelector && n.push(this.customStyleSelector), n
                }
                 function l() {
                    var e = {},
                        i = this.options;
                    i.showLink || (i.elements.link = "", i.elements.messagelink = i.elements.message), Object.keys(i.elements).forEach(function(n) {
                        e[n] = t.interpolateString(i.elements[n], function(e) {
                            var t = i.content[e];
                            return e && "string" == typeof t && t.length ? t : ""
                        })
                    });
                    var n = i.compliance[i.type];
                    n || (n = i.compliance.info), e.compliance = t.interpolateString(n, function(t) {
                        return e[t]
                    });
                    var o = i.layouts[i.layout];
                    return o || (o = i.layouts.basic), t.interpolateString(o, function(t) {
                        return e[t]
                    })
                }
                 function u(i) {
                    var n = this.options,
                        o = document.createElement("div"),
                        s = n.container && 1 === n.container.nodeType ? n.container : document.body;
                    o.innerHTML = i;
                    var r = o.children[0];
                    return r.style.display = "none", t.hasClass(r, "cc-window") && e.hasTransition && t.addClass(r, "cc-invisible"), this.onButtonClick = h.bind(this), r.addEventListener("click", this.onButtonClick), n.autoAttach && (s.firstChild ? s.insertBefore(r, s.firstChild) : s.appendChild(r)), r
                }
                 function h(n) {
                    var o = t.traverseDOMPath(n.target, "cc-btn") || n.target;
                    if (t.hasClass(o, "cc-btn")) {
                        var s = o.className.match(new RegExp("\\bcc-(" + i.join("|") + ")\\b")),
                            r = s && s[1] || !1;
                        r && (this.setStatus(r), this.close(!0))
                    }
                    t.hasClass(o, "cc-close") && (this.setStatus(e.status.dismiss), this.close(!0)), t.hasClass(o, "cc-revoke") && this.revokeChoice()
                }
                 function p(e) {
                    var i = t.hash(JSON.stringify(e)),
                        n = "cc-color-override-" + i,
                        o = t.isPlainObject(e);
                    return this.customStyleSelector = o ? n : null, o && d(i, e, "." + n), o
                }
                 function d(i, n, o) {
                    if (e.customStyles[i]) return void++e.customStyles[i].references;
                    var s = {},
                        r = n.popup,
                        a = n.button,
                        c = n.highlight;
                    r && (r.text = r.text ? r.text : t.getContrast(r.background), r.link = r.link ? r.link : r.text, s[o + ".cc-window"] = ["color: " + r.text, "background-color: " + r.background], s[o + ".cc-revoke"] = ["color: " + r.text, "background-color: " + r.background], s[o + " .cc-link," + o + " .cc-link:active," + o + " .cc-link:visited"] = ["color: " + r.link], a && (a.text = a.text ? a.text : t.getContrast(a.background), a.border = a.border ? a.border : "transparent", s[o + " .cc-btn"] = ["color: " + a.text, "border-color: " + a.border, "background-color: " + a.background], a.padding && s[o + " .cc-btn"].push("padding: " + a.padding), "transparent" != a.background && (s[o + " .cc-btn:hover, " + o + " .cc-btn:focus"] = ["background-color: " + (a.hover || v(a.background))]), c ? (c.text = c.text ? c.text : t.getContrast(c.background), c.border = c.border ? c.border : "transparent", s[o + " .cc-highlight .cc-btn:first-child"] = ["color: " + c.text, "border-color: " + c.border, "background-color: " + c.background]) : s[o + " .cc-highlight .cc-btn:first-child"] = ["color: " + r.text]));
                    var l = document.createElement("style");
                    document.head.appendChild(l), e.customStyles[i] = {
                        references: 1,
                        element: l.sheet
                    };
                    var u = -1;
                    for (var h in s) s.hasOwnProperty(h) && l.sheet.insertRule(h + "{" + s[h].join(";") + "}", ++u)
                }
                 function v(e) {
                    return e = t.normaliseHex(e), "000000" == e ? "#222" : t.getLuminance(e)
                }
                 function f(i) {
                    if (t.isPlainObject(i)) {
                        var n = t.hash(JSON.stringify(i)),
                            o = e.customStyles[n];
                        if (o && !--o.references) {
                            var s = o.element.ownerNode;
                            s && s.parentNode && s.parentNode.removeChild(s), e.customStyles[n] = null
                        }
                    }
                }
                 function m(e, t) {
                    for (var i = 0, n = e.length; i < n; ++i) {
                        var o = e[i];
                        if (o instanceof RegExp && o.test(t) || "string" == typeof o && o.length && o === t) return !0
                    }
                    return !1
                }
                 function b() {
                    var i = this.setStatus.bind(this),
                        n = this.close.bind(this),
                        o = this.options.dismissOnTimeout;
                    "number" == typeof o && o >= 0 && (this.dismissTimeout = window.setTimeout(function() {
                        i(e.status.dismiss), n(!0)
                    }, Math.floor(o)));
                    var s = this.options.dismissOnScroll;
                    if ("number" == typeof s && s >= 0) {
                        var r = function(t) {
                            window.pageYOffset > Math.floor(s) && (i(e.status.dismiss), n(!0), window.removeEventListener("scroll", r), this.onWindowScroll = null)
                        };
                        this.options.enabled && (this.onWindowScroll = r, window.addEventListener("scroll", r))
                    }
                    var a = this.options.dismissOnWindowClick,
                        c = this.options.ignoreClicksFrom;
                    if (a) {
                        var l = function(o) {
                            for (var s = !1, r = o.path.length, a = c.length, u = 0; u < r; u++)
                                if (!s)
                                    for (var h = 0; h < a; h++) s || (s = t.hasClass(o.path[u], c[h]));
                            s || (i(e.status.dismiss), n(!0), window.removeEventListener("click", l), this.onWindowClick = null)
                        }.bind(this);
                        this.options.enabled && (this.onWindowClick = l, window.addEventListener("click", l))
                    }
                }
                 function g() {
                    if ("info" != this.options.type && (this.options.revokable = !0), t.isMobile() && (this.options.animateRevokable = !1), this.options.revokable) {
                        var e = a.call(this);
                        this.options.animateRevokable && e.push("cc-animate"), this.customStyleSelector && e.push(this.customStyleSelector);
                        var i = this.options.revokeBtn.replace("", e.join(" ")).replace("", this.options.content.policy);
                        this.revokeBtn = u.call(this, i);
                        var n = this.revokeBtn;
                        if (this.options.animateRevokable) {
                            var o = t.throttle(function(e) {
                                var i = !1,
                                    o = 20,
                                    s = window.innerHeight - 20;
                                t.hasClass(n, "cc-top") && e.clientY < o && (i = !0), t.hasClass(n, "cc-bottom") && e.clientY > s && (i = !0), i ? t.hasClass(n, "cc-active") || t.addClass(n, "cc-active") : t.hasClass(n, "cc-active") && t.removeClass(n, "cc-active")
                            }, 200);
                            this.onMouseMove = o, window.addEventListener("mousemove", o)
                        }
                    }
                }
                var y = {
                    enabled: !0,
                    container: null,
                    cookie: {
                        name: "cookieconsent_status",
                        path: "/",
                        domain: "",
                        expiryDays: 365,
                        secure: !1
                    },
                    onPopupOpen: function() {},
                    onPopupClose: function() {},
                    onInitialise: function(e) {},
                    onStatusChange: function(e, t) {},
                    onRevokeChoice: function() {},
                    onNoCookieLaw: function(e, t) {},
                    content: {
                        header: "Cookies used on the website!",
                        message: "This website uses cookies to ensure you get the best experience on our website.",
                        dismiss: "Got it!",
                        allow: "Allow cookies",
                        deny: "Decline",
                        link: "Learn more",
                        href: "https://cookiesandyou.com",
                        close: "❌",
                        target: "_blank",
                        policy: "Cookie Policy"
                    },
                    elements: {
                        header: ' ',
                        message: '',
                        messagelink: ' ',
                        dismiss: '',
                        allow: '',
                        deny: '',
                        link: '',
                        close: ''
                    },
                    window: '',
                    revokeBtn: '
    ', compliance: { info: '
    ', "opt-in": '
    ', "opt-out": '
    ' }, type: "info", layouts: { basic: "", "basic-close": "", "basic-header": "" }, layout: "basic", position: "bottom", theme: "block", "static": !1, palette: null, revokable: !1, animateRevokable: !0, showLink: !0, dismissOnScroll: !1, dismissOnTimeout: !1, dismissOnWindowClick: !1, ignoreClicksFrom: ["cc-revoke", "cc-btn"], autoOpen: !0, autoAttach: !0, whitelistPage: [], blacklistPage: [], overrideHTML: null }; return n.prototype.initialise = function(e) { this.options && this.destroy(), t.deepExtend(this.options = {}, y), t.isPlainObject(e) && t.deepExtend(this.options, e), r.call(this) && (this.options.enabled = !1), m(this.options.blacklistPage, location.pathname) && (this.options.enabled = !1), m(this.options.whitelistPage, location.pathname) && (this.options.enabled = !0); var i = this.options.window.replace("", c.call(this).join(" ")).replace("", l.call(this)), n = this.options.overrideHTML; if ("string" == typeof n && n.length && (i = n), this.options["static"]) { var o = u.call(this, '
    ' + i + "
    "); o.style.display = "", this.element = o.firstChild, this.element.style.display = "none", t.addClass(this.element, "cc-invisible") } else this.element = u.call(this, i); b.call(this), g.call(this), this.options.autoOpen && this.autoOpen() }, n.prototype.destroy = function() { this.onButtonClick && this.element && (this.element.removeEventListener("click", this.onButtonClick), this.onButtonClick = null), this.dismissTimeout && (clearTimeout(this.dismissTimeout), this.dismissTimeout = null), this.onWindowScroll && (window.removeEventListener("scroll", this.onWindowScroll), this.onWindowScroll = null), this.onWindowClick && (window.removeEventListener("click", this.onWindowClick), this.onWindowClick = null), this.onMouseMove && (window.removeEventListener("mousemove", this.onMouseMove), this.onMouseMove = null), this.element && this.element.parentNode && this.element.parentNode.removeChild(this.element), this.element = null, this.revokeBtn && this.revokeBtn.parentNode && this.revokeBtn.parentNode.removeChild(this.revokeBtn), this.revokeBtn = null, f(this.options.palette), this.options = null }, n.prototype.open = function(t) { if (this.element) return this.isOpen() || (e.hasTransition ? this.fadeIn() : this.element.style.display = "", this.options.revokable && this.toggleRevokeButton(), this.options.onPopupOpen.call(this)), this }, n.prototype.close = function(t) { if (this.element) return this.isOpen() && (e.hasTransition ? this.fadeOut() : this.element.style.display = "none", t && this.options.revokable && this.toggleRevokeButton(!0), this.options.onPopupClose.call(this)), this }, n.prototype.fadeIn = function() { var i = this.element; if (e.hasTransition && i && (this.afterTransition && s.call(this, i), t.hasClass(i, "cc-invisible"))) { if (i.style.display = "", this.options["static"]) { var n = this.element.clientHeight; this.element.parentNode.style.maxHeight = n + "px" } var r = 20; this.openingTimeout = setTimeout(o.bind(this, i), r) } }, n.prototype.fadeOut = function() { var i = this.element; e.hasTransition && i && (this.openingTimeout && (clearTimeout(this.openingTimeout), o.bind(this, i)), t.hasClass(i, "cc-invisible") || (this.options["static"] && (this.element.parentNode.style.maxHeight = ""), this.afterTransition = s.bind(this, i), i.addEventListener(e.transitionEnd, this.afterTransition), t.addClass(i, "cc-invisible"))) }, n.prototype.isOpen = function() { return this.element && "" == this.element.style.display && (!e.hasTransition || !t.hasClass(this.element, "cc-invisible")) }, n.prototype.toggleRevokeButton = function(e) { this.revokeBtn && (this.revokeBtn.style.display = e ? "" : "none") }, n.prototype.revokeChoice = function(e) { this.options.enabled = !0, this.clearStatus(), this.options.onRevokeChoice.call(this), e || this.autoOpen() }, n.prototype.hasAnswered = function(t) { return Object.keys(e.status).indexOf(this.getStatus()) >= 0 }, n.prototype.hasConsented = function(t) { var i = this.getStatus(); return i == e.status.allow || i == e.status.dismiss }, n.prototype.autoOpen = function(e) { !this.hasAnswered() && this.options.enabled ? this.open() : this.hasAnswered() && this.options.revokable && this.toggleRevokeButton(!0) }, n.prototype.setStatus = function(i) { var n = this.options.cookie, o = t.getCookie(n.name), s = Object.keys(e.status).indexOf(o) >= 0; Object.keys(e.status).indexOf(i) >= 0 ? (t.setCookie(n.name, i, n.expiryDays, n.domain, n.path, n.secure), this.options.onStatusChange.call(this, i, s)) : this.clearStatus() }, n.prototype.getStatus = function() { return t.getCookie(this.options.cookie.name) }, n.prototype.clearStatus = function() { var e = this.options.cookie; t.setCookie(e.name, "", -1, e.domain, e.path) }, n }(), e.Location = function() { function e(e) { t.deepExtend(this.options = {}, s), t.isPlainObject(e) && t.deepExtend(this.options, e), this.currentServiceIndex = -1 } function i(e, t, i) { var n, o = document.createElement("script"); o.type = "text/" + (e.type || "javascript"), o.src = e.src || e, o.async = !1, o.onreadystatechange = o.onload = function() { var e = o.readyState; clearTimeout(n), t.done || e && !/loaded|complete/.test(e) || (t.done = !0, t(), o.onreadystatechange = o.onload = null) }, document.body.appendChild(o), n = setTimeout(function() { t.done = !0, t(), o.onreadystatechange = o.onload = null }, i) } function n(e, t, i, n, o) { var s = new(window.XMLHttpRequest || window.ActiveXObject)("MSXML2.XMLHTTP.3.0"); if (s.open(n ? "POST" : "GET", e, 1), s.setRequestHeader("Content-type", "application/x-www-form-urlencoded"), Array.isArray(o)) for (var r = 0, a = o.length; r < a; ++r) { var c = o[r].split(":", 2); s.setRequestHeader(c[0].replace(/^\s+|\s+$/g, ""), c[1].replace(/^\s+|\s+$/g, "")) } "function" == typeof t && (s.onreadystatechange = function() { s.readyState > 3 && t(s) }), s.send(n) } function o(e) { return new Error("Error [" + (e.code || "UNKNOWN") + "]: " + e.error) } var s = { timeout: 5e3, services: ["ipinfo"], serviceDefinitions: { ipinfo: function() { return { url: "//ipinfo.io", headers: ["Accept: application/json"], callback: function(e, t) { try { var i = JSON.parse(t); return i.error ? o(i) : { code: i.country } } catch (n) { return o({ error: "Invalid response (" + n + ")" }) } } } }, ipinfodb: function(e) { return { url: "//api.ipinfodb.com/v3/ip-country/?key={api_key}&format=json&callback={callback}", isScript: !0, callback: function(e, t) { try { var i = JSON.parse(t); return "ERROR" == i.statusCode ? o({ error: i.statusMessage }) : { code: i.countryCode } } catch (n) { return o({ error: "Invalid response (" + n + ")" }) } } } }, maxmind: function() { return { url: "//js.maxmind.com/js/apis/geoip2/v2.1/geoip2.js", isScript: !0, callback: function(e) { return window.geoip2 ? void geoip2.country(function(t) { try { e({ code: t.country.iso_code }) } catch (i) { e(o(i)) } }, function(t) { e(o(t)) }) : void e(new Error("Unexpected response format. The downloaded script should have exported `geoip2` to the global scope")) } } } } }; return e.prototype.getNextService = function() { var e; do e = this.getServiceByIdx(++this.currentServiceIndex); while (this.currentServiceIndex < this.options.services.length && !e); return e }, e.prototype.getServiceByIdx = function(e) { var i = this.options.services[e]; if ("function" == typeof i) { var n = i(); return n.name && t.deepExtend(n, this.options.serviceDefinitions[n.name](n)), n } return "string" == typeof i ? this.options.serviceDefinitions[i]() : t.isPlainObject(i) ? this.options.serviceDefinitions[i.name](i) : null }, e.prototype.locate = function(e, t) { var i = this.getNextService(); return i ? (this.callbackComplete = e, this.callbackError = t, void this.runService(i, this.runNextServiceOnError.bind(this))) : void t(new Error("No services to run")) }, e.prototype.setupUrl = function(e) { var t = this.getCurrentServiceOpts(); return e.url.replace(/\{(.*?)\}/g, function(i, n) { if ("callback" === n) { var o = "callback" + Date.now(); return window[o] = function(t) { e.__JSONP_DATA = JSON.stringify(t) }, o } if (n in t.interpolateUrl) return t.interpolateUrl[n] }) }, e.prototype.runService = function(e, t) { var o = this; if (e && e.url && e.callback) { var s = e.isScript ? i : n, r = this.setupUrl(e); s(r, function(i) { var n = i ? i.responseText : ""; e.__JSONP_DATA && (n = e.__JSONP_DATA, delete e.__JSONP_DATA), o.runServiceCallback.call(o, t, e, n) }, this.options.timeout, e.data, e.headers) } }, e.prototype.runServiceCallback = function(e, t, i) { var n = this, o = function(t) { s || n.onServiceResult.call(n, e, t) }, s = t.callback(o, i); s && this.onServiceResult.call(this, e, s) }, e.prototype.onServiceResult = function(e, t) { t instanceof Error || t && t.error ? e.call(this, t, null) : e.call(this, null, t) }, e.prototype.runNextServiceOnError = function(e, t) { if (e) { this.logError(e); var i = this.getNextService(); i ? this.runService(i, this.runNextServiceOnError.bind(this)) : this.completeService.call(this, this.callbackError, new Error("All services failed")) } else this.completeService.call(this, this.callbackComplete, t) }, e.prototype.getCurrentServiceOpts = function() { var e = this.options.services[this.currentServiceIndex]; return "string" == typeof e ? { name: e } : "function" == typeof e ? e() : t.isPlainObject(e) ? e : {} }, e.prototype.completeService = function(e, t) { this.currentServiceIndex = -1, e && e(t) }, e.prototype.logError = function(e) { var t = this.currentServiceIndex, i = this.getServiceByIdx(t); console.warn("The service[" + t + "] (" + i.url + ") responded with the following error", e) }, e }(), e.Law = function() { function e(e) { this.initialise.apply(this, arguments) } var i = { regionalLaw: !0, hasLaw: ["AT", "BE", "BG", "HR", "CZ", "CY", "DK", "EE", "FI", "FR", "DE", "EL", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "SK", "ES", "SE", "GB", "UK", "GR", "EU"], revokable: ["HR", "CY", "DK", "EE", "FR", "DE", "LV", "LT", "NL", "PT", "ES"], explicitAction: ["HR", "IT", "ES"] }; return e.prototype.initialise = function(e) { t.deepExtend(this.options = {}, i), t.isPlainObject(e) && t.deepExtend(this.options, e) }, e.prototype.get = function(e) { var t = this.options; return { hasLaw: t.hasLaw.indexOf(e) >= 0, revokable: t.revokable.indexOf(e) >= 0, explicitAction: t.explicitAction.indexOf(e) >= 0 } }, e.prototype.applyLaw = function(e, t) { var i = this.get(t); return i.hasLaw || (e.enabled = !1, "function" == typeof e.onNoCookieLaw && e.onNoCookieLaw(t, i)), this.options.regionalLaw && (i.revokable && (e.revokable = !0), i.explicitAction && (e.dismissOnScroll = !1, e.dismissOnTimeout = !1)), e }, e }(), e.initialise = function(i, n, o) { var s = new e.Law(i.law); n || (n = function() {}), o || (o = function() {}); var r = Object.keys(e.status), a = t.getCookie("cookieconsent_status"), c = r.indexOf(a) >= 0; return c ? void n(new e.Popup(i)) : void e.getCountryCode(i, function(t) { delete i.law, delete i.location, t.code && (i = s.applyLaw(i, t.code)), n(new e.Popup(i)) }, function(t) { delete i.law, delete i.location, o(t, new e.Popup(i)) }) }, e.getCountryCode = function(t, i, n) { if (t.law && t.law.countryCode) return void i({ code: t.law.countryCode }); if (t.location) { var o = new e.Location(t.location); return void o.locate(function(e) { i(e || {}) }, n) } i({}) }, e.utils = t, e.hasInitialised = !0, window.cookieconsent = e } }(window.cookieconsent || {});

  2. Custom JavaScript (cookie-consent-script.js) to position the popup created by cookieconsent
    Including the cookieconsent library into your website
    <script
      src="https://cdn.jsdelivr.net/npm/cookieconsent@3.1.0/build/cookieconsent.min.js"
      data-cfasync="false">
    </script>
/* Consent Banner and Clarity Consent API Integration */

window.addEventListener('load', function () {
  if (window.cookieconsent && window.cookieconsent.initialise) {
    window.cookieconsent.initialise({
      "palette": {
        "popup": {
          "background": "#134376", // Background color of the consent popup
          "text": "#9DDCFF"        // Text color within the consent popup
        },
        "button": {
          "background": "#9DDCFF", // Background color of the dismiss button
          "text": "#134376"        // Text color of the dismiss button
        }
      },
      "position": "bottom-right",
      "theme": "classic",
      "type": "opt-in",
      "content": {
        "message": "This website uses cookies to analyze performance and provide basic functionality.",
        "dismiss": "Decline",
        "allow": "Accept",
        "link": "Learn more",
        "href": "/privacy-policy.html"
      },
      onInitialise: function (status) {
        if (status === "allow") {
          window.clarity('consent', true);
        } else {
          window.clarity('consent', false);
        }
      },
      onStatusChange: function (status) {
        if (status === "allow") {
          window.clarity('consent', true);
        } else {
          window.clarity('consent', false);
        }
      }
    });
  }
});

All three of the script tags discussed above are assembled below. The cookieconsent library must be loaded before the Clarity script is invoked.

  1. The cookieconsent library
  2. My custom JavaScript file (cookie-consent-script.js)
  3. The Clarity script invocation with my website ID redacted

This is how I put it all together in my Jekyll HTML template. Every page in my site incorporates this template.

_layouts/base.html fragment
<script src="https://cdn.jsdelivr.net/npm/cookieconsent@3.1.0/build/cookieconsent.min.js"
  data-cfasync="false"></script>
<script src="/assets/js/cookie-consent-script.js"></script>
<script>
(function (c, l, a, r, i, t, y) {
  c[a] = c[a] || function () { (c[a].q = c[a].q || []).push(arguments) };
  t = l.createElement(r); t.async = 1; t.src = "https://www.clarity.ms/tag/" + i + "?ref=bwt";
  y = l.getElementsByTagName(r)[0]; y.parentNode.insertBefore(t, y);
  })(window, document, "clarity", "script", "secretstuffhere");
</script>

The privacy policy must discuss Clarity’s analytics cookies.

A standardized stylesheet from cookieconsent exists. You can also use this nice form.

This stylesheet is horrible; I discuss an alternative in the Styling section below.

Test the Setup

Run your website locally and display the dialog as described below.

  1. Interact with the banner: Click Accept or Decline and verify behavior.
  2. In browser dev tools (F12 / Application / Cookies), confirm no Clarity cookies (e.g., _clck, _clsk) are set when consent is declined.

Deploy

  1. Deploy changes to each site.
  2. Visit each domain to simulate an EEA/UK location. You can use a VPN or a browser extension.
  3. In the Clarity dashboard (clarity.microsoft.com), go to Settings / Consent / Check Consent Mode status (it will show green if it is working). See Microsoft’s Consent verification guide.
  4. Test on multiple browsers (e.g., Chrome, Firefox) to ensure compatibility.

Monitor and Maintain

  • Periodically check Clarity’s dashboard for consent signal errors.
  • If issues arise, email clarityms@microsoft.com with your project IDs.
  • Re-test before October 31, 2025, to ensure compliance.

Styling

The CSS provided for cookieconsent is compressed; when formatted you can see how large it is.

Including the cookie consent stylesheet
<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/cookieconsent@3.1.0/build/cookieconsent.min.css"
/>

Here is the formatted version of the CSS provided by cookieconsent.

Formatted cookieconsent.css
.cc-window {
opacity: 1;
transition: opacity 1s ease
}

.cc-window.cc-invisible {
opacity: 0
}

.cc-animate.cc-revoke {
transition: transform 1s ease
}

.cc-animate.cc-revoke.cc-top {
transform: translateY(-2em)
}

.cc-animate.cc-revoke.cc-bottom {
transform: translateY(2em)
}

.cc-animate.cc-revoke.cc-active.cc-bottom,
.cc-animate.cc-revoke.cc-active.cc-top,
.cc-revoke:hover {
transform: translateY(0)
}

.cc-grower {
max-height: 0;
overflow: hidden;
transition: max-height 1s
}

.cc-link,
.cc-revoke:hover {
text-decoration: underline
}

.cc-revoke,
.cc-window {
position: fixed;
overflow: hidden;
box-sizing: border-box;
font-family: Helvetica, Calibri, Arial, sans-serif;
font-size: 16px;
line-height: 1.5em;
display: -ms-flexbox;
display: flex;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
z-index: 9999
}

.cc-window.cc-static {
position: static
}

.cc-window.cc-floating {
padding: 2em;
max-width: 24em;
-ms-flex-direction: column;
flex-direction: column
}

.cc-window.cc-banner {
padding: 1em 1.8em;
width: 100%;
-ms-flex-direction: row;
flex-direction: row
}

.cc-revoke {
padding: .5em
}

.cc-header {
font-size: 18px;
font-weight: 700
}

.cc-btn,
.cc-close,
.cc-link,
.cc-revoke {
cursor: pointer
}

.cc-link {
opacity: .8;
display: inline-block;
padding: .2em
}

.cc-link:hover {
opacity: 1
}

.cc-link:active,
.cc-link:visited {
color: initial
}

.cc-btn {
display: block;
padding: .4em .8em;
font-size: .9em;
font-weight: 700;
border-width: 2px;
border-style: solid;
text-align: center;
white-space: nowrap
}

.cc-highlight .cc-btn:first-child {
background-color: transparent;
border-color: transparent
}

.cc-highlight .cc-btn:first-child:focus,
.cc-highlight .cc-btn:first-child:hover {
background-color: transparent;
text-decoration: underline
}

.cc-close {
display: block;
position: absolute;
top: .5em;
right: .5em;
font-size: 1.6em;
opacity: .9;
line-height: .75
}

.cc-close:focus,
.cc-close:hover {
opacity: 1
}

.cc-revoke.cc-top {
top: 0;
left: 3em;
border-bottom-left-radius: .5em;
border-bottom-right-radius: .5em
}

.cc-revoke.cc-bottom {
bottom: 0;
left: 3em;
border-top-left-radius: .5em;
border-top-right-radius: .5em
}

.cc-revoke.cc-left {
left: 3em;
right: unset
}

.cc-revoke.cc-right {
right: 3em;
left: unset
}

.cc-top {
top: 1em
}

.cc-left {
left: 1em
}

.cc-right {
right: 1em
}

.cc-bottom {
bottom: 1em
}

.cc-floating>.cc-link {
margin-bottom: 1em
}

.cc-floating .cc-message {
display: block;
margin-bottom: 1em
}

.cc-window.cc-floating .cc-compliance {
-ms-flex: 1 0 auto;
flex: 1 0 auto
}

.cc-window.cc-banner {
-ms-flex-align: center;
align-items: center
}

.cc-banner.cc-top {
left: 0;
right: 0;
top: 0
}

.cc-banner.cc-bottom {
left: 0;
right: 0;
bottom: 0
}

.cc-banner .cc-message {
display: block;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
max-width: 100%;
margin-right: 1em
}

.cc-compliance {
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
-ms-flex-line-pack: justify;
align-content: space-between
}

.cc-floating .cc-compliance>.cc-btn {
-ms-flex: 1;
flex: 1
}

.cc-btn+.cc-btn {
margin-left: .5em
}

@media print {

.cc-revoke,
.cc-window {
display: none
}
}

@media screen and (max-width:900px) {
.cc-btn {
white-space: normal
}
}

@media screen and (max-width:414px) and (orientation:portrait),
screen and (max-width:736px) and (orientation:landscape) {
.cc-window.cc-top {
top: 0
}

.cc-window.cc-bottom {
bottom: 0
}

.cc-window.cc-banner,
.cc-window.cc-floating,
.cc-window.cc-left,
.cc-window.cc-right {
left: 0;
right: 0
}

.cc-window.cc-banner {
-ms-flex-direction: column;
flex-direction: column
}

.cc-window.cc-banner .cc-compliance {
-ms-flex: 1 1 auto;
flex: 1 1 auto
}

.cc-window.cc-floating {
max-width: none
}

.cc-window .cc-message {
margin-bottom: 1em
}

.cc-window.cc-banner {
-ms-flex-align: unset;
align-items: unset
}

.cc-window.cc-banner .cc-message {
margin-right: 0
}
}

.cc-floating.cc-theme-classic {
padding: 1.2em;
border-radius: 5px
}

.cc-floating.cc-type-info.cc-theme-classic .cc-compliance {
text-align: center;
display: inline;
-ms-flex: none;
flex: none
}

.cc-theme-classic .cc-btn {
border-radius: 5px
}

.cc-theme-classic .cc-btn:last-child {
min-width: 140px
}

.cc-floating.cc-type-info.cc-theme-classic .cc-btn {
display: inline-block
}

.cc-theme-edgeless.cc-window {
padding: 0
}

.cc-floating.cc-theme-edgeless .cc-message {
margin: 2em 2em 1.5em
}

.cc-banner.cc-theme-edgeless .cc-btn {
margin: 0;
padding: .8em 1.8em;
height: 100%
}

.cc-banner.cc-theme-edgeless .cc-message {
margin-left: 1em
}

.cc-floating.cc-theme-edgeless .cc-btn+.cc-btn {
margin-left: 0
}

Displaying the Dialog

Firefox displayed the dialog fine.

I was unable to make this work on Google Chrome because https://cdn.jsdelivr.net/npm/cookieconsent@3.1.0/build/cookieconsent.min.js was blocked by the client.

I saved the cookieconsent.min.js file locally and referenced it from my site. This did not help Chrome.

Then I disabled the uBlock Origin Lite extension for Chrome and the dialog appeared. Google Chrome recently changed the rules for third-party cookies and how manifests work. No-one except Alphabet (the parent company of Google) wanted this change, but Alphabet is driven by advertising revenue and is now happy to be as evil as necessary to win.

I am not going to worry about this. I expect most users will not have uBlock Origin Lite installed, and fewer people use that browser plugin every day. I have complied with the regulations, and that is enough.

Clear cookieconsent_status

The cookie consent popup is created by cookie-consent-script.js. It appears only if the cookieconsent_status cookie is not set. To see the popup again, you must delete this cookie. Open the browser's JavaScript console by using F12 / Console, and type the following:

JavaScript
document.cookie = "cookieconsent_status=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";
window.location.reload();

This deletes the cookieconsent_status cookie and reloads the page, triggering the popup as if this is the first time you visited the page.

Wrapping this code into a function will make it easy to clear the cookieconsent_status cookie:

Wrapped into a JavaScript function
function cc() {
  document.cookie = "cookieconsent_status=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";
  window.location.reload();
}

Merely type cc() in the JavaScript console to see the cookie consent dialog.

Clear All Domain Cookies

You can also clear all cookies for the domain. This is useful if you want to test the cookie consent dialog and you do not want to manually delete each cookie. The following function clears all cookies for the domain and reloads the page.

Another JavaScript function
function ccc() {
  document.cookie.split(";").forEach(
    function(c) {
      document.cookie = c.replace(/^ +/, "")
                        .replace(/=.*/,
                                  "=;expires=" +
                                    new Date().toUTCString() + ";path=/"
                                  );
    }
  );
  window.location.reload();
}

The ccc() function clears all cookies for the domain and reloads the page, causing the popup to appear.

* indicates a required field.

Please select the following to receive Mike Slinn’s newsletter:

You can unsubscribe at any time by clicking the link in the footer of emails.

Mike Slinn uses Mailchimp as his marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp’s privacy practices.