Mike Slinn

Microsoft Clarity Lets Me Watch You Click and Scroll

Published 2022-03-31. Last modified 2025-09-16.
Time to read: 7 minutes.

This page is part of the posts collection, categorized under Jekyll, Microsoft, SEO, Software-Expert.

I spent a fair bit of time designing the plugins for this Jekyll-powered website for SEO. These plugins are published as open source; click on the word Jekyll in the sentence above this paragraph to learn more. You are welcome!

I do not use Google Analytics because it slows down page load time dramatically. I do use Google Search Console, however, and recently started trying out Microsoft Bing Webmaster Tools.

Microsoft Clarity and Hotjar

The day before April Fool's Day in 2022, I stumbled across Microsoft Clarity, a free product that I knew nothing about. I was curious to know what benefit it might provide.

I learned that one of the things it can do is provide videos of actual user sessions as they interact with the website. Those videos are fantastic.

As a writer of a lot of online content, I have spent hours watching you, dear readers. Individually yet anonymously. Watching people read teaches you a lot about what is important to them. As a writer, knowing your audience allows you to be more relevant. BTW, I do not know who or where my readers are, just the country they are in.

Check out this video!

This is one of the first user sessions that Microsoft Clarity recorded for this website.

As you can see, Microsoft Clarity lets me watch movies of users clicking and scrolling through my website; spooky yet very informative.

Clarity is open source. Here is the GitHub project.

I have since learned that Hotjar is similar to Microsoft Clarity.

Online Behavior Matches Real-World Behavior

The user in the above video read a bit about my experience as a software expert witness, then straightaway downloaded my resume. They were on the website for just over one minute. Although they did call me, their online behavior showed a lack of urgency, and their ‘real-world’ behavior mirrored what I saw in the movie.

A week later another party visited my site, and spent 80 minutes carefully reading 3 pages, among others. Their ‘real-world’ behavior also matched their online behavior, in that they exhibited a sense of urgency towards engaging a software expert.

Tracking Downloads And Other Behavior

Microsoft Clarity does not consider a download as a click. It does not even notice downloads. For me, downloads are what I care about most. If you never download my resume, you probably are not a candidate for hiring me as a software expert. I want to track downloads, not clicks. Clicks are nice, but there is a direct relationship between resume downloads and signing contracts with legal firms who represent new clients.

AWS CloudTrail and CloudWatch can provide download details and much more. For example, user IP addresses and geographic location can be captured when a monitored event occurs.

Many Websites Perform Surveillance

Wired Magazine published an article on a similar type of surveillance (keyloggers) on May 11, 2022. I paraphrased two sentences from that article:

  • 1.8% of websites studied gathered an EU user's email address without their consent, and a staggering 2.95% logged a US user's email.
  • For US users, 8.4% of sites may have been leaking data to Meta, Facebook’s parent company, and 7.4% of sites may be impacted for EU users.
 – From Thousands of Popular Websites See What You Type—Before You Hit Submit

Yours truly does nothing of the sort.

Update 2023-07-10

I no longer need to watch the videos of user action. Microsoft Clarity's AI generates summaries. Often it yields incredibly actionable information, but the Key Takeaways section is generally way off the mark. YMMV.

Today I noticed the insights tab, visible when viewing an entry for a ‘hit’. Seems like Microsoft has employed AI to generate the following impressive summary of the user's behavior. I added punctuation, a light edit and a whimsical pad of notepaper.

Visited URL matches regex: ^https://www\.mslinn\.com/softwareexpert/index\.html(\?.*)?$
Last 7 days

Session insights

Some key takeaways from this session are:

The user was interested in the software expert witness and computer expert services offered by Mike Slinn, as they visited the homepage from Google and spent about four minutes there.

The user was also curious about the technology expert article series, especially the ones related to Git and version control systems. They visited the article index page multiple times and clicked on several articles, spending about 15 minutes in total on this topic.

The user was most engaged with the article on libgit2, a library that provides low-level access to Git operations. They spent about one and a half minutes on this article and clicked on a link to git-fame, a tool that shows the contribution statistics of a Git repository.

The user frequently resized their browser window, which may indicate that they were using a mobile device or adjusting their screen for better readability. The user also switched between tabs or applications often, as indicated by the page hidden and page visible events. This may suggest that they were multitasking or comparing information from different sources.

01:47 / 47:31

Update 2023-07-18

Today I discovered the Clarity Live plugin for the Google Chrome web browser. There is no such plugin for Firefox, sadly.

View instant heatmaps right on your live site and watch recent session recordings for any page you are on with our extension.

  • GDPR & CCPA ready
  • No sampling
  • Built on open source
 – From Microsoft Clarity

Update 2023-10-04

I just stumbled across the Clarity Client API.

You can quickly get started with Clarity without coding but by interacting with the Clarity client API. This API can help you access advanced features as described in this reference. Add the following calls to Clarity APIs to the HTML or JavaScript of your webpage to access these features.

Note

Your Clarity ID serves as your API key. No other client API key is necessary, and there is no cost for using Clarity client APIs.

Popular Pages

The data that Clarity manages can be downloaded with the Clarity Data Export API. API calls are limited to 10 calls per project, per day; that is not a problem. The endpoint is www.clarity.ms/export-data/api/v1/project-live-insights.

An access token is required to use the API. To get one, open the Clarity project and select Settings -> Data Export -> Generate new API token. I called my token clarity_data_export and stored it in an environment variable called CLARITY_DATA

Here is how I retrieved the information about the most popular pages in the last 30 days using the bash command line. This bash script requires jq to be installed:

#!/bin/bash

DAYS=30
DIM1="PopularPages"
PARAMS="\numOfDays=$DAYS&dimension1=$DIM1" # Backslash escape
URL="https://www.clarity.ms/export-data/api/v1/project-live-insights"

# curl -s \
#   --trace - \
#   --location "$URL?$PARAMS" \
#   --header 'Content-Type: application/json' \
#   --header "Authorization: Bearer $CLARITY_DATA" | less
# exit

JSON=$(curl -s \
  --location "$URL?$PARAMS" \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer $CLARITY_DATA"
)

echo "$JSON" | jq -r '.[] | select(.metricName == "PopularPages").information[].url'

The result is something like the following:

Results
https://www.mslinn.com/blog/2023/09/14/boost.html
https://www.mslinn.com/blog/2021/02/11/javascript-named-arguments.html
https://www.mslinn.com/blog/2023/08/20/pytest.html
https://www.mslinn.com/blog/2022/01/10/wsl-backup.html
https://www.mslinn.com/av_studio/640-music21.html
https://www.mslinn.com/llm/4000-llm-wsl.html
https://www.mslinn.com/blog/2021/04/28/buildah-podman.html
https://www.mslinn.com/blog/2020/10/27/installing-a-new-ssh-key-on-awc-ec2-with-user-data.html
https://www.mslinn.com/av_studio/570-ableton-push-standalone.html
https://www.mslinn.com/git/600-partial-clone.html

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

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

This looked like it might be a lot of work. The simplest free solution is detailed below, following these images of what the solution will look like.

The image is taken from a Clarity recording of a an anonymous Norweigan user session. 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 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.

CookieConsent stores user consent for up to a year.

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

Add CookieConsent JavaScript

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: "&#x274c;", target: "_blank", policy: "Cookie Policy" }, elements: { header: '<span class="cc-header"></span>&nbsp;', message: '<span id="cookieconsent:desc" class="cc-message"></span>', messagelink: '<span id="cookieconsent:desc" class="cc-message"> <a aria-label="learn more about cookies" role=button tabindex="0" class="cc-link" href="" rel="noopener noreferrer nofollow" target=""></a></span>', dismiss: '<a aria-label="dismiss cookie message" role=button tabindex="0" class="cc-btn cc-dismiss"></a>', allow: '<a aria-label="allow cookies" role=button tabindex="0" class="cc-btn cc-allow"></a>', deny: '<a aria-label="deny cookies" role=button tabindex="0" class="cc-btn cc-deny"></a>', link: '<a aria-label="learn more about cookies" role=button tabindex="0" class="cc-link" href="" rel="noopener noreferrer nofollow" target=""></a>', close: '<span aria-label="dismiss cookie message" role=button tabindex="0" class="cc-close"></span>' }, window: '<div role="dialog" aria-live="polite" aria-label="cookieconsent" aria-describedby="cookieconsent:desc" class="cc-window "><!--googleoff: all--><!--googleon: all--></div>', revokeBtn: '<div class="cc-revoke "></div>', compliance: { info: '<div class="cc-compliance"></div>', "opt-in": '<div class="cc-compliance cc-highlight"></div>', "opt-out": '<div class="cc-compliance cc-highlight"></div>' }, 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, '<div class="cc-grower">' + i + "</div>"); 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 || {});

    Include the above cookieconsent library before your version of the Clarity script.

    Below you can see a reference to the cookieconsent library, followed by my Clarity script:

    _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>
  2. Custom JavaScript to position the popup created by cookieconsent
    Including the cookieconsent library into your website
    <script
      src="https://cdn.jsdelivr.net/npm/cookieconsent@3/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);
        }
      }
    });
  }
});

The privacy policy must discuss Clarity’s analytics cookies.

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

Use it in an HTML document like this:

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

I wish I knew how to make the popup appear on demand. That would allow me to fuss with the styling. I do not use a VPN and I do not want to get one because I believe VPNs decrease security. Currently I just deploy a new version of the website after making a cosmetic change, then I lurk on the Clarity site until a visitor from the EU comes along so I can watch the video of their visit. This is the only way I know to see how the popup looks. Hopefully someone will educate me soon.

The above CSS is compressed; when formatted you can see how large it is. Surely this is much more than is required to style a simple Yes/No popup.

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
}

Test the Setup

  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. Interact with the banner: Click Accept or Decline and verify behavior.
  4. In browser dev tools (F12 / Application / Cookies), confirm no Clarity cookies (e.g., _clck, _clsk) are set when consent is declined.
  5. 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.
  6. 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.

Notes

  • Customization: Adjust the banner’s palette, position, or content in the script to match your site’s design (see CookieConsent docs).
  • Limitations: This is a basic opt-in banner. For advanced features (e.g., category-based consent), you’d need a more complex script or a CMP like CookieYes (not free). This setup meets Clarity’s requirements for GDPR compliance.
  • CMS Users: If your sites use a CMS (e.g., WordPress), you can use a free plugin like Complianz (free tier) to manage the banner and call Clarity’s API.
  • Alternative: If you want a fully custom banner without CookieConsent, you’d need to code a consent UI and store user choices (e.g., in localStorage), then call window.clarity('consent', true/false) based on user input. This is more complex and not recommended unless you’re experienced with JavaScript. See Clarity Consent API docs.

Looking Ahead

I would very much like to receive a continuous data feed of the above AI-generated per-visit summary, in real time as it becomes available. An email stream would be a quick way to start. A streaming api for data would be a logical next step.

Perhaps the Clarity API might be a place to start.

* 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.