When it came time to add a cookie consent banner, I wanted something lightweight—what a shame it would be to gum up a nice, clean Jekyll site with a bunch of bloated Javascript! The Jekyll Codex cookie consent was close, but I wanted to give visitors the option of customizing which cookies they wanted to accept (e.g., website analytics vs. ad personalization). The Jekyll Codex example also functions by reloading the page with the cookie code when the visitor consents, which struck me as unfortunately cumbersome—I would rather have a solution that invisibly works with the underlying JavaScript in the background so as not to disrupt the user experience.
Documentation
for gtag
consent mode
Happily, Google recently released a beta for a new
“Consent Mode” feature
that makes this process easy. With the consent mode feature, you specify a
default for whether cookies are used for ads or for analytics, which you can
then update when a visitors consent or changes their preferences with a simple
call to gtag()
. You can even specify
different defaults by region,
for example to comply with strict consent-first regulations in Europe while
applying a more liberal opt-out policy in other jurisdictions.
Here’s what I came up with. First, we have the code for the banner itself:
Even though I style the banner to appear at the bottom of the screen, the
code needs to go at the top of the <body>
so that screen readers will
provide the consent content to visitors before launching into the rest of
the page. Similarly, the aria-label
for the consent block will be
important when we get to the button that allows visitors to change their
settings.
The banner consists of a brief explanatory message and three buttons: a
button to accept the cookies, a link to the privacy policy, and a button to
customize which cookies the user wants to accept or deny. These are followed
by a customization block (cookie-consent-customize
) with controls for each
type of cookies (here, website analytics and ad personalization). The
customization block is hidden by default.
Clicking on the consent button calls the recordConsent()
function (below)
to record the visitor’s choices and hides the consent block. Clicking on the
“Customize” button displays the customization block and hides the “Customize”
button, since it no longer has a function once the customization block is
displayed.
The selectors for each cookie type are courtesy of
w3schools.
The slider simply consists of a filled background block of class .slider
(to create the track for the switch) and an attached filled foreground block
(to represent the toggle) implemented with .slider:before
. The slider is
linked with an invisible checkbox input. When the input is checked, the toggle
shape is translated to the right and the background color shifts from grey to
green. An added shadow indicates when the control has the keyboard focus
(made possible with tab-index: 0
). This is all accomplished via CSS:
To get the banner to stick to the bottom of the screen, we use
position: fixed
, and z-index: 999
places the banner on top of everything
that scrolls underneath it:
The recordConsent()
function sets three cookies: one for each consent type
(analytics and ads), as well as a convenience cookie that indicates that the
consent banner has been dismissed. It then calls gtag()
with the updated
consent settings:
Now for the part that links it all together. We augment the typical gtag
code with default consent settings, then check to see if the user has
dismissed the consent banner. If so, we apply the user’s consent settings.
All of this happens before calling gtag('config')
so that any saved
settings from a previous page view will apply right out of the gate:
Although Google recommends putting gtag('config')
in <head>
, this block
needs to go after the cookie-consent
block so that the middle section can
read the status of the checkbox elements.
Note: I am not a GDPR legal expert.
Google Analytics assigns a random identifier to each visitor that is unique to the specific website being analyzed. Unless a website designer explicitly (or negligently) incorporates identifying information into the analytics stream (say, after the user logs in), these “identifiers” are, for all intents and purposes, anonymous. I have no idea the identity of user “1066186681.1615103843” from Parsons, Kansas (population 9,736). Nevertheless, European courts apparently consider these random identifiers as “personally identifiable information” for which positive consent is needed before they can be sent to a third-party for processing under the General Data Protection Regulation (GDPR). As such, for the default settings, I chose to deny analytics cookies only for visitors from the EU and UK (allowing users from other regions to opt-out). For ad personalization, in contrast, I chose to wait until users from any region provide positive consent because some users can find personalized ads a little creepy.
The readCookie()
function comes from the
Jekyll Codex implementation
and simply searches through this page’s cookies for the named value:
Finally, users need a means for revoking their consent once given. This simply requires redisplaying the customization block of the consent banner to allow users to change their preferences and re-record the result. I accomplish this using a simple button on the privacy page:
When a user clicks the button, the customize block and consent blocks are
displayed, the customize button is hidden, and the keyboard focus is set to
the “OK” button in the consent banner. Setting the focus is important, here,
for visitors who use screen readers. Without this, there would be no
detectable feedback indicating that an action had occurred when the button
was activated, and users might have difficulty knowing that they need to
navigate backward to find the consent banner at the beginning of the page
(DOM-wise). The aria-label
for the consent banner helps here, as well, to
let the user know that this “OK” button is in the context of the consent
banner.
And there we have it! The package comes in at about 4kB, which could probably be reduced a little by minification. All in all, a fairly lightweight solution, I think.