Simpler cookie notices
It’s hard to browse the web without coming across the ubiquitous cookie notice these days. In fact, it’s become a source of annoyance for many people. Privacy regulations oblige countless website owners to ask their visitors for consent before using any cookies that aren’t necessary for the site to function. Generally, those are cookies used for tracking and targeting.
In this article, I’ll present a pattern for a simple cookie notice. I mostly aim to demonstrate the ease of the native dialog element and of the Cookie Store API; two recent or recently improved web features that greatly reduce our effort. In doing so, I’ll be playing devil’s advocate as I’d rather see all forms of tracking disappear, and with it the need for these notices. In other words, only use this if you must.
Permission for analytics?
Cookies used in website analytics are deemed non-essential. That means you need a consent notice that informs your visitors and gathers their explicit consent before setting such cookies on their device. For many websites, this is the only additional source of cookies, meaning we can be concise and specific in how we present the choice to the user. The term “cookie” acts as a placeholder for any similar technology here, so switching to Local Storage won’t relieve you from an explicit opt-in. In fact, let me discourage you one more time.
I believe there’s a strict hierarchy of options regarding tracking and analytics. Ranked from best to worst:
- Don’t track anything. Soon, humanity shall unite in everlasting peace and prosperity.
- Use privacy-friendly analytics like Fathom (referral link) or Plausible. These don’t use cookies and anonymise all collected data, so no consent banner is required.
- Use traditional analytics like Google Analytics, but ask for consent before doing so.
- Disregard your users and silently track them anyway. Your version of hell—or a lawsuit—awaits.
We’re concerned with the third option here. Let’s get to it.
The markup
As announced, we’ll use the dialog element. This native dialog implementation suffered a bunch of problems for a long time, but recent changes to the HTML specification have caused it to mature to the point of generally being the preferred option over rolling out a custom version. Accessibility expert Scott O’Hara—who’s been monitoring and testing the dialog for years—gave the green light back in January.
The markup is elegant and looks like this:
The heading and paragraph text provide context to the dialog, especially when paired with the aria-labelledby
and aria-describedby
roles. The true novelty is the method="dialog"
attribute on the enclosed form. Thanks to this, clicking the buttons will close the dialog without any JavaScript intervention.
The dialog’s closed by default, so you won’t see it at this point if you’re coding along. We’ll open it later with a touch of JavaScript, but for now we can make it appear by adding the open
attribute to the dialog element, like so:
Then we’ll add some styling.
Styling
We’ll style our cookie notice as a non-modal dialog that sits in the bottom right corner of the screen. The dialog being non-modal means you can still interact with the rest of the webpage.
The restrictions on max-inline-size
and max-block-size
ensure the notice doesn’t overflow the viewport on smaller screens and stays clear of the edges. By adding overflow-y: auto
, the dialog becomes scrollable in the vertical direction if needed. If we’ve scrolled all the way down, overscroll-behavior-block
prevents the rest of the page to start scrolling too. The remaining styles provide consistent spacing between the elements in our dialog. Adding some extra cosmetics, the notice now looks something like this:
The bottom right positioning is clearer at full scale, though. Make sure to remove the open
attribute before heading to the next step.
Opening and closing the dialog
Let’s get our cookie notice to work. On page load, we should check for any stored preferences. If the user previously consented to the use of tracking cookies, the analytics script can go ahead and run. If we don’t find any preferences, we’ll show the dialog so the user can make a choice. We’ll save that choice in a cookie.
This is where the native dialog really starts to shine. Opening the dialog is as easy as calling the show()
method—no fiddling with ARIA roles or classes. We then add an event listener for the close
event. Remember how the “Accept” and “Reject” buttons automatically close the dialog through the method="dialog"
attribute on the parent form. This will also trigger the close
event. The returnValue
property on our dialog is now set to the value
attribute of whichever button we clicked.
As a last step, we should add the undefined getCookie
and setCookie
functions.
Old and new cookies
Getting and setting cookies the old way is surprisingly cumbersome. It involves weird string manipulation on the document.cookie
property. Luckily, there’s the newer Cookie Store API with its simple get()
and set()
methods. Browser support isn’t that great yet, with notably Firefox and Safari missing, but we’ll use the old cookie methods as a fallback. Here’s the code for the missing functions:
Now you may wonder why we go through the trouble of supporting two ways of getting and setting cookies, when Local Storage provides the same functionality with perfect browser support. The answer lies in the expiration date: items in Local Storage never expire. According to the GDPR, consent must be renewed at least once a year, and some national data protection hardliners like the French CNIL even recommend renewal every six months. Setting a cookie that expires ensures we comply with these regulations.
Here’s the result as a standalone or as an embedded version. The messages in the console will tell you if the settings cookie was found or not, and if analytics should run or not.
Permission for even more things?
What if our website uses other non-essential cookies besides the ones used for analytics? Privacy laws stipulate that users should receive granular control over the types of data collecting they do or do not agree to. That means we have to adapt our cookie notice to include several categories. We’ll make use of checkboxes. Let’s have a look.
A bit more markup and styling
We’ve added a third cookie category to the mix: cookies set by YouTube embeds. Each category gets a checkbox that’s linked to a descriptive paragraph through the aria-describedby
role on the input. The box corresponding to essential cookies is checked and disabled, as users can’t opt out of these. The “Accept” and “Reject” buttons are now merged into a single button that saves the user’s preferences. Notice how we link to the privacy policy—or possibly the cookie policy—of the website. That is where we should give a more elaborate overview of the types and purposes of the cookies we use.
We’ll add just a dash more CSS:
Our cookie notice grew a bit bigger and now looks like this:
I’ve added the “YouTube” category on purpose, to demonstrate yet another way to go. In my article on privacy-friendly video embeds, I showed how you can replace the YouTube video iframe with an overlay that prompts the user to consent to YouTube’s cookies before loading the video. I called this just-in-time consent, because we asked the user for consent only at the time it’s needed. For analytics, that means on page load of any page; but video embeds aren’t usually the first thing you land on when visiting a website. We could employ the overlay strategy for the videos, in which case we fall back to the very simple cookie notice we made before.
A bit more cookies
In this case, it’s best to store the preference for each category in a separate cookie. That way, we can query these preferences independently as needed. The alterations to our JavaScript code are pretty self-explanatory.
And here’s the final result, as a standalone page and as an embed:
Conclusion
If you must use a cookie notice, then it might as well be a lightweight and an accessible one. The native dialog element does a lot of the heavy lifting, and the Cookie Store API makes storing and retrieving cookies a breeze.