Menu Search
Jump to the content X X
Smashing Conf New York

You know, we use ad-blockers as well. We gotta keep those servers running though. Did you know that we publish useful books and run friendly conferences — crafted for pros like yourself? E.g. our upcoming SmashingConf New York, dedicated to smart front-end techniques and design patterns.

When Your Code Has To Work: Complying With Legal Mandates

Douglas Crockford famously declared browsers to be “the most hostile software engineering environment imaginable,” and that wasn’t hyperbole. Ensuring that our websites work across a myriad of different devices, screen sizes and browsers our users depend on to access the web is a tall order, but it’s necessary. If our websites don’t enable users to accomplish the key tasks they come to do, we’ve failed them.

We should do everything in our power to ensure our websites function under even the harshest of scenarios, but at the same, we can’t expect our users to have the exact same experience in every browser, on every device. Yahoo realized this more than a decade ago and made it a central concept in its “Graded Browser Support1” strategy:

Support does not mean that everybody gets the same thing. Expecting two users using different browser software to have an identical experience fails to embrace or acknowledge the heterogeneous essence of the Web. In fact, requiring the same experience for all users creates an artificial barrier to participation. Availability and accessibility of content should be our key priority.

And that was a few years before the iPhone was introduced!

Providing alternate experience pathways for our core functionality should be a no-brainer, but when it comes to implementing stuff we’d rather not think about, we often reach for the simplest drop-in solution, despite the potential negative impact it could have on our business.

Consider the EU’s “cookie law.”2 If you’re unfamiliar, this somewhat contentious law3 is privacy legislation that requires websites to obtain consent from visitors before storing or retrieving information from their device. We call it the cookie law, but the legislation also applies to web storage4, IndexedDB5 and other client-side data storage and retrieval APIs.

Compliance with this law is achieved by:

  1. notifying users that the website requires the ability to store and read information on their device;
  2. providing a link to the website’s privacy statement, which includes information about the storage mechanisms being used and what they are being used for;
  3. prompting users to confirm their acceptance of this requirement.

If you operate a website aimed at folks living in the EU and fail to do this, you could be subject to a substantial fine. You could even open yourself up to a lawsuit.

If you’ve had to deal with the EU cookie law before, you’re probably keenly aware that a ton of “solutions” are available to provide compliance. Those quotation marks are fully intentional because nearly every one I found — including the one provided by the EU6 itself — has been a drop-in JavaScript file that enables compliance. If we’re talking about the letter of the law, however, they don’t actually. The problem is that, as awesome and comprehensive as some of these solutions are, we can never be guaranteed that our JavaScript programs will actually run7. In order to truly comply with the letter of the law, we should provide a fallback version of the utility — just in case. Most people will never see it, but at least we know we’re covered if something goes wrong.

I stumbled into this morass while building the 10k Apart contest website8. We weren’t using cookies for much on the website — mainly analytics and vote-tracking — but we were using the Web Storage API9 to speed up the performance of the website and to save form data temporarily while folks were filling out the form. Because the contest was open to folks who live in the EU, we needed to abide by the cookie law. And because none of the solutions I found actually complied with the law in either spirit or reality — the notable exception being WordPress’ EU Cookie Law10 plugin, which works both with and without JavaScript, but the contest website wasn’t built in WordPress or even PHP, so I had to do something else — I opted to roll my own robust solution.

Planning It Out Link

I’m a big fan of using interface experience (IX) maps11 to diagram functionality. I find their simple nature easy to understand and to tweak as I increase the fidelity of an experience. For this feature, I started with a (relatively) simple IX map that diagrammed what would happen when a user requests a page on the website.

IX map for the basic interaction and fallback.12

IX map for the basic interaction and fallback. (View large version13)

This IX map outlines several potential experiences that vary based on the user’s choice and feature availability. I’ll walk through the ideal scenario first:

  1. A user comes to the website for the first time. The server checks to see whether they have accepted the use of cookies and web storage but doesn’t find anything.
  2. The server injects a banner into the HTML, containing the necessary messaging and a form that, when submitted, confirms acceptance.
  3. The browser renders the page with the banner.
  4. The user clicks to accept the use of cookies and web storage.
  5. Client-side JavaScript sets the accepts cookie and closes the banner.
  6. On subsequent page requests, the server reads the accepts cookie and does not inject the banner code. JavaScript sees the cookie and enables the cookie and web storage code.

For the vast majority of users, this is the experience they’ll get, and that’s awesome. That said, however, we can never be 100% guaranteed our client-side JavaScript code will run, so we need a backup plan. Here’s the fallback experience:

  1. A user comes to the website for the first time. The server checks to see whether they have accepted the use of cookies and web storage but doesn’t find anything.
  2. The server injects a banner into the HTML, containing the necessary messaging and a form that, when submitted, confirms acceptance.
  3. The browser renders the page with the banner.
  4. The user clicks to accept the use of cookies and web storage.
  5. The click initiates a form post to the server, which responds by setting the accepts cookie before redirecting the user back to the page they were on.
  6. On subsequent page requests, the server reads the accepts cookie and does not inject the banner code.
  7. If JavaScript becomes available later, it will see the cookie and enable its cookie and web storage code.

Not bad. There’s an extra roundtrip to the server, but it’s a quick one, and, more importantly, it provides a foolproof fallback in the absence of our preferred JavaScript-driven option. True, it could fall victim to a networking issue, but there’s not much we can do to mitigate that without JavaScript in play.

Speaking of mitigating networking issues, the 10k Apart contest website uses a service worker14 to do some pretty aggressive caching; the service worker intercepts any page request and supplies a cached version if one exists. That could result in users getting a copy of the page with the banner still in it, even if they’ve already agreed to allow cookies. Time to update the IX map.

IX map for the basic interaction and fallback.15

The original IX map, adjusted to handle cached pages. (View large version16)

This is one of the reasons I like IX maps so much: They are really easy to generate and simple to update when you want to add features or handle more scenarios. With a few adjustments in place, I can account for the scenario in which a stale page includes the banner unnecessarily and have JavaScript remove it.

With this plan in place, it was time to implement it.

Server-Side Implementation Link

10k Apart’s back end is written in Node.js17 and uses Express18. I’m not going to get into the nitty-gritty of our installation and configuration, but I do want to talk about how I implemented this feature. First off, I opted to use Express’ cookie-parser19 middleware to let me get and set the cookie.

// enable cookie-parser for Express
var cookieParser = require('cookie-parser');

Once that was set up, I created my own custom Express middleware20 that would intercept requests and check for the approves_cookies cookie:

var checkCookie = function(req, res, next) {
  res.locals.approves_cookies = ( req.cookies['approves_cookies'] === 'yes' );
  res.locals.current_url = req.url || '/';

This code establishes a middleware function named checkCookie(). All Express middleware gets access to the request (req), the response (res) and the next middleware function (next), so you’ll see those accounted for as the three arguments to that function. Then, within the function, I am modifying the response object to include two local variables (res.locals) to capture whether the cookie has already been set (res.locals.approves_cookies) and the currently requested URL (res.locals.current_url). Then, I call the next middleware function.

With that written, I can include this middleware in Express:


All of the templates for the website are Mustache21 files, and Express automatically pipes res.locals into those templates. Knowing that, I created a Mustache partial22 to handle the banner:

  <div id="cookie-banner" role="alert">
    <form action="/cookies-ok" method="post">
      <input type="hidden" name="redirect_to" value="{{current_url}}">
      <p>This site uses cookies for analytics and to track voting. If you're interested, more details can be found in <a href="{{privacy_url}}#maincookiessimilartechnologiesmodule">our cookie policy</a>.</p>
      <button type="submit">I'm cool with that</button>

This template uses an inverted section23 that only renders the div when approves_cookies is false. Within that markup, you can also see the current_url getting piped into a hidden input to indicate where a user should be redirected if the form method of setting the cookie is used. You remembered: the fallback.

Speaking of the fallback, since we have one, we also need to handle that on the server side. Here’s the Node.js code for that:

var affirmCookies = function (req, res) {
  if ( ! req.cookies['approves_cookies'] )
    res.cookie('approves_cookies', 'yes', {
      secure: true,
      maxAge: ( 365 * 24 * 60 * 60 ) // 1 year
};'/cookies-ok', affirmCookies);

This ensures that if the form is submitted, Express will respond by setting the approves_cookies cookie (if it’s not already set) and then redirecting the user to the page they were on. Taken altogether, this gives us a solid baseline experience for every user.

Now, it’s worth noting that none of this code is going to be useful to you if your projects don’t involve the specific stack I was working with on this project (Node.js, Express, Mustache). That said, the logic I’ve outlined here and in the IX map is portable to pretty much any language or framework you happen to know and love.

OK, let’s switch gears and work some magic on the front end.

Front-End Implementation Link

When JavaScript is available and running properly, we’ll want to take full advantage of it, but it doesn’t make sense to run any code against the banner if it doesn’t exist, so first things first: I should check to see whether the banner is even in the page.

var $cookie_banner = document.getElementById('cookie-banner');

if ( $cookie_banner )
  // actual code will go here

In order to streamline the application logic, I’m going to add another conditional within to check for the accepts_cookies cookie. I know from my second pass on the IX map there’s an outside chance that the banner might be served up by my service worker even if the accepts cookie exists, so checking for the cookie early lets me run only the bit of JavaScript that removes the banner. But before I jump into all of that, I’ll create a function I can call in any of my code to let me know whether the user has agreed to let me cookie them:

function cookiesApproved(){
  return document.cookie.indexOf('approves_cookies') > -1;

I need this check in multiple places throughout my JavaScript, so it makes sense to break it out into a separate function. Now, let’s revisit my banner-handling logic:

var $cookie_banner = document.getElementById('cookie-banner');

if ( $cookie_banner )

  // banner exists but cookie is set
  if ( cookiesApproved() )
    // hide the banner immediately!
  // cookie has not been set 
    // add the logic to set the cookie
    // and close the banner


Setting cookies in JavaScript is a little convoluted because you need to set it as a string, but it’s not too ghastly. I broke out the process into its own function so that I could set it as an event handler on the form:

function approveCookies( e ) {

  // prevent the form from submitting

  var cookie,               // placeholder for the cookie
      expires = new Date(); // start building expiry date

  // expire in one year
  expires.setFullYear( expires.getFullYear() + 1 );

  // build the cookie
  cookie = [
    'expires=' + expires.toUTCString(),
    'domain=' + window.location.hostname,
    window.location.protocol == 'https:' ? 'secure' : ''

  // set it
  document.cookie = cookie.join('; ');

  // close up the banner

  // return
  return false;


// find the form inside the banner
var $form = $cookie_banner.getElementsByTagName('form')[0];

// hijack the submit event
$form.addEventListener( 'submit', approveCookies, false );

The comments in the code should make it pretty clear, but just in case, here’s what I’m doing:

  1. Hijack the form submission event (e) and cancel its default action using e.preventDefault().
  2. Use the Date object to construct a date one year out.
  3. Assemble the bits of the cookie, including the approves_cookies value, the expiry date, the domain the cookie is bound to, and whether the cookie should be secure (so I can test locally).
  4. Set document.cookie equal to the assembled cookie string.
  5. Trigger a separate method — closeCookieBanner() — to close the banner (which I will cover in a moment).

With that in place, I can define closeCookieBanner() to handle, well, closing up the banner. There are actually two instances in which I need this functionality: after setting the cookie (as we just saw) and if the service worker serves up a stale page that still has the banner in it. Even though each requires roughly the same functionality, I want to make the stale-page cleanup version a little more aggressive. Here’s the code:

function closeCookieBanner( immediate ) {

  // How fast to close? Animation takes .5s
  var close_speed = immediate ? 0 : 600;

  // remove

    $cookie_banner.parentNode.removeChild( $cookie_banner );

    // remove the DOM reference
    $cookie_banner = null;

  }, close_speed);

  // animate closed
  if ( ! immediate ) {
    $cookie_banner.className = 'closing';


This function takes a single optional argument. If true (or anything “truthy”24) is passed in, the banner is immediately removed from the page (and its reference is deleted). If no argument is passed in, that doesn’t happen for 0.6 seconds, which is 0.1 seconds after the animation finishes up (we’ll get to the animation momentarily). The class change triggers that animation.

You already saw one instance of this function referenced in the previous code block. Here it is in the cached template branch of the conditional you saw earlier:

// banner exists but cookie is set
if ( cookiesApproved() )
  // close immediately
  closeCookieBanner( true );

Adding Some Visual Sizzle Link

Because I brought up animations, I’ll discuss the CSS I’m using for the cookie banner component, too. Like most implementations of cookie notices, I opted for a visual full-width banner. On small screens, I wanted the banner to appear above the content and push it down the page. On larger screens I opted to affix it to the top of the viewport because it would not obstruct reading to nearly the same degree as it would on a small screen. Accomplishing this involved very little code:

#cookie-banner {
  background: #000;
  color: #fff;
  font-size: .875rem;
  text-align: center;

@media (min-width: 60em) {
  #cookie-banner {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 1000;      

Using the browser’s default styles, the cookie banner already displays block, so I didn’t really need to do much apart from set some basic text styles and colors. For the large screen (the “full-screen” version comes in at 60 ems), I affix it to the top of the screen using position: fixed, with a top offset of 0. Setting its left and right offsets to 0 ensures it will always take up the full width of the viewport. I also set the z-index quite high so it sits on top of everything else in the stack.

Here’s the result:

A video showing the browser viewport being enlarged from 240 to 960 pixels width. When the design hits the 60-em breakpoint, the banner becomes fixed-positioned.

Once the basic design was there, I took another pass to spice it up a bit. I decided to have the banner animate in and out using CSS. First things first: I created two animations. Initially, I tried to run a single animation in two directions for each state (opening and closing) but ran into problems triggering the reversal — you might be better at CSS animations than I am, so feel free to give it a shot. In the end, I also decided to tweak the two animations to be slightly different, so I’m fine with having two of them:

@keyframes cookie-banner {
  0% {
    max-height: 0;
  100% {
    max-height: 20em;
@keyframes cookie-banner-reverse {
  0% {
    max-height: 20em;
  100% {
    max-height: 0;
    display: none;

Not knowing how tall the banner would be (this is responsive design, after all), I needed it to animate to and from a height of auto. Thankfully, Nikita Vasilyev25 published a fantastic overview of how to transition values to and from auto26 a few years back. In short, animate max-height instead. The only thing to keep in mind is that the size of the non-zero max-height value you are transitioning to and from needs to be larger than your max, and it will also directly affect the speed of the animation. I found 20 ems to be more than adequate for this use case, but your project may require a different value.

It’s also worth noting that I used display: none at the conclusion of my cookie-banner-reverse animation (the closing one) to ensure the banner becomes unreachable to users of assistive technology such as screen readers. It’s probably unnecessary, but I did it as a failsafe just in case something happens and JavaScript doesn’t remove the banner from the DOM.

Wiring it up required only a few minor tweaks to the CSS:

#cookie-banner {
  box-sizing: border-box;
  overflow: hidden;
  animation: cookie-banner 1s 1s linear forwards;

#cookie-banner.closing {
  animation: cookie-banner-reverse .5s linear forwards;

This assigned the two animations to the two different banner states: The opening and resting state, cookie-banner, runs for one second after a one-second delay; the closing state, cookie-banner-reverse, runs for only half a second with no delay. I am using a class of closing, set via the JavaScript I showed earlier, to trigger the state change. Just for completeness, I’ll note that this code also stabilizes the dimensions of the banner with box-sizing: border-box and keeps the contents from spilling out of the banner using overflow: hidden.

One last bit of CSS tweaking and we’re done. On small screens, I’m leaving a margin between the cookie notice (#cookie-banner) and the page header (.banner). I want that to go away when the banner collapses, even if the cookie notice is not removed from the DOM. I can accomplish that with an adjacent-sibling selector:

#cookie-banner + .banner {
  transition: margin-top .5s;

#cookie-banner.closing + .banner {
  margin-top: 0;

It’s worth noting that I am setting the top margin on every element but the first one, using Heydon Pickering’s clever “lobotomized owl27” selector. So, the transition of margin-top on .banner will be from a specific value (in my case, 1.375 rem) to 0. With this code in place, the top margin will collapse over the same duration as the one used for the closing animation of the cookie banner and will be triggered by the very same class addition.

A video showing the final implementation on a wide screen, both with and without JavaScript.

Simple, Robust, Resilient Link

What I like about this approach is that it is fairly simple. It took only about an hour or two to research and implement, and it checks all of the compliance boxes with respect to the EU law. It has minimal dependencies, offers several fallback options, cleans up after itself and is a relatively back-end-agnostic pattern.

When tasked with adding features we may not like — and, yes, I’d count a persistent nagging banner as one of those features — it’s often tempting to throw some code at it to get it done and over with. JavaScript is often a handy tool to accomplish that, especially because the logic can often be self-contained in an external script, configured and forgotten. But there’s a risk in that approach: JavaScript is never guaranteed28. If the feature is “nice to have,” you might be able to get away with it, but it’s probably not a good idea to play fast and loose with a legal mandate like this. Taking a few minutes to step back and explore how the feature can be implemented with minimal effort on all fronts will pay dividends down the road. Believe me29.

(rb, vf, il, al)

Front page image credit: Pexels30.

Footnotes Link

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30

↑ Back to top Tweet itShare on Facebook

As would be expected from a former manager of the Web Standards Project, Aaron Gustafson is passionate about web standards and accessibility. He has been working on the Web for two decades now and is a web standards advocate at Microsoft, working closely with their browser team. He writes about whatever’s on his mind at

  1. 1

    Good article Aaron, but the cookie law expires in 12 months. It’s being folder into a new data protection reform which removes the onus from the website, and onto the browser. The likelihood of anyone being penalised in the mean time is highly unlikely. The fundamental point you raise is valid regardless.

    You may have to remove anything you have implemented now in a few months, so for a law that is the digital equivalent of the UK law where it’s illegal to be drunk in a pub…maybe it’s not really worth the effort.

    • 2

      Aaron Gustafson

      March 2, 2017 4:52 pm

      I’m glad that’s happening, honestly. As you noted, it still pays to think about ways to ensure you comply with legal requirements. The cookie law was just an easy example (and one I had to deal with recently).

      • 3

        Absolutely and your thought that went into your solution was enlightening. Great stuff :)

  2. 4

    And if the end user clears our their cookies and cache on closing of the browser window, they will have to go through that process again, unless you store that information per user on the server side, e.g if access to your website/web app is based on login, then you should store it as part of the user profile, that they have accepted this “cookie banner”

    • 5

      Aaron Gustafson

      March 2, 2017 5:01 pm

      Completely agree. We didn’t really have a login function on the site (except during the brief voting period), so it didn’t apply in this example.

  3. 6

    Heather Burns

    March 2, 2017 3:44 pm

    Thank you for the trackback. That being said, Michael’s comment is correct. The draft of the law’s revamp, expected to take effect on 25 May 2018, shifts the compliance responsibility from web site owners to browser and app manufacturers.

    While I’m grateful for the time you’ve taken to walk through the techncial aspects of this particular compliance requirement, this tutorial proves the exact problem with this particular law. You’ve coded for compliance, but not actually done anything to facilitate the user’s privacy. What about data minimalisation? Third party integration and tracking? The information captured when a user registers an account? The ad networks you use? What is your data retention policy for contest entries? Where is the user’s data being transferred? Are you providing the user with the information they need to make an informed decision? Have you actually done the hard work offline to ask, answer, and document these questions?

    The 2018 revamp requires site and app administrators to be fully accountable through privacy notices about all of that that information. Cookie law compliance becomes a matter of responsibility, not UX, and I would encourage readers to brush up on what those requirements will be.

    In that vein, framing compliance in the scaremongering vein of “do this or you will get a fine” is doing a disservice to Smashing’s audience. (Fines and penalties, for what it’s worth, are not how complaince works with this particular regulation. They are only deployed after all constructive opportunities have been exhausted – and in fact, the number of cookie law fines levied across Europe can be counted on one hand.)

    Compliance based on the 2018 revamp rules is about doing the hard work to safeguard your users. It’s not about resentfully deploying a window to avoid a penalty. It never was.

    • 7

      Aaron Gustafson

      March 2, 2017 5:00 pm

      You are absolutely right, there’s a lot more that goes into full compliance. For what it’s worth, I did discuss several considerations that fall squarely into the front-end developer’s lap: cookies (obviously), but also offline caching by way of `localStorage` and `sessionStorage`. It’s also worth considering that ServiceWorker, AppCache, and other client-side storage mechanisms should all be made to comply. All of the items you bring up are also critical.

      For what it’s worth, the point of this article was not to talk about the cookie law specifically (or its replacement), but rather to highlight that we often follow the path of least resistance in implementing things we see as a checkbox in a list (accessibility is another common one) rather than actually trying to tackle the underlying issue in a robust way. Folks should absolutely go above and beyond this implementation to improve the privacy of their users, but to tackle all of the items you discuss would require way more than an article… perhaps an entire book.

      • 8

        Heather Burns

        March 2, 2017 5:25 pm

        Thanks, Aaron, I appreciate that. I think we agree that any front-end compliance can only be a reflection of the thought that goes into the back-end and offline processes.

        And I hope to finish that book next week…

  4. 9

    Stanislaw Olszak

    March 2, 2017 5:25 pm

    Great article, but as Michael pointed out the “cookie law” expires in near future.

    The interesting thing is that the law requires website owner to obtain user consent but everyone just focuses on fancy ways of implementing annoying cookie banners. The real issue here is “consent” which may imply options. One can argue should the user opt in or out, but I strongly believe that the point here is to provide an option for cookieless website usage. In some cases, it may prove to be difficult if not impossible, but if we really want to obey the law and have users privacy and best interest in mind, we should at least consider giving a choice.

    I also agree with Heather’s comment on the subject. Privacy is what matters here, not another bulletproof way of pushing banners to users.

    I guess the law itself might be the problem since it fails to clearly communicate its intentions (as do most company lawyers enforcing it). In response dev community created banners that only gave birth to software dedicated to removing them. Just take a look at add blockers specs or look up Banner Hunter Safari extension. It’s pretty ironic that we put so much work into something that people want to remove anyway. Not exactly what we were aiming for, right?

    Nevertheless, I really enjoyed the technical take on the issue. Sorry for going a little off-topic here, but the title Complying With Legal Madness… sorry Mandates kind of provoked it ;)

  5. 10

    Thanks for an interesting article that raises some valid points for a lot of cases beyond the silly cookie banners.

    I usually just slap a quick JS plugin on display the annoying banner (for my own sites, I don’t do anything – my little bit of civil disobedience), as I believe the whole law is worse than pointless:

    First, cookies are essential for many website features. Without them, you can’t log in, you can’t do e-commerce etc.

    Second, every browser on the planet already has all the controls a user could want if they don’t want cookies.

    Third, no one actually reads that messages. Like other frequent information/error/warning messages, user have been conditioned to click OK. They don’t read the message at all.

    I do care about privacy, but this law is the perfect example of politicians wanting to be seen “doing something” about some issue that is current in the media, but not at all understanding the technology. Certainly, cookies can be used for behavioral tracking (flight prices going up when they know you’re interested etc.). That behavior should absolutely be illegal.
    Also, third party cookies (Facebook etc.) are certainly problematic – but they can be perfectly controlled with existing options in browsers. Instead of an intelligent law dealing with the actual problems of cookies, we have gotten a law where users have just learned to click the OK button to “make that annoying thing covering up the page go away”

    Thankfully, as others have stated, the law is finally going away – hopefully.


Leave a Comment

You may use simple HTML to add links or lists to your comment. Also, use <pre><code class="language-*">...</code></pre> to mark up code snippets. We support -js, -markup and -css for comments.

↑ Back to top