Cookie Conset

@use 'sprucecss/scss/spruce' as *;

.cookie-consent-helper {
  display: flex;
  inset-block-end: spacer('m');
  inset-inline: spacer('m');
  justify-content: center;
  pointer-events: none;
  position: fixed;
  z-index: z-index('toast');
}

.cookie-consent {
  align-items: center;
  background-color: color('surface');
  border: config('border-width-base', $display) solid color('border');
  border-radius: radius('lg');
  box-shadow: shadow('xl');
  color: color('text');
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  gap: spacer('s') spacer('m');
  justify-content: center;
  max-inline-size: 48rem;
  padding: spacer('s');
  pointer-events: all;
  text-align: center;
  translate: 0 200%;

  &--slidein {
    animation: slidein config('duration', $transition) forwards;
  }

  &--slideout {
    animation: slideout config('duration', $transition) forwards;
  }

  &__caption {
    margin-block: 0;
  }

  &__btns {
    display: flex;
    gap: spacer('s');
  }
}

@keyframes slidein {
  from {
    translate: 0 200%;
  }

  to {
    translate: 0 0;
  }
}

@keyframes slideout {
  from {
    translate: 0 0;
  }

  to {
    translate: 0 200%;
  }
}

@include motion-reduce {
  .cookie-consent--slidein,
  .cookie-consent--slideout {
    animation-duration: 1ms;
  }
}
<button class="btn btn--outline-primary" type="button" data-action="cookie" data-type="analytics" data-on-text="Decline" data-off-text="Accept">analytics cookies</button>
import Cookie from '../../../../js/qkie.js';

(() => {
  const cookie = new Cookie('demo-gdpr-');
  const btns = document.querySelectorAll('button[data-action="cookie"]');
  let caption = '';
  let consentModal = null;
  let redirect = false;

  if (
    !cookie.isset('analytics') &&
    !cookie.isset('denied')
  ) {
    caption = `<div class="cookie-consent-helper"><section class="cookie-consent cookie-consent--slidein" aria-label="Cookie consent">
        <p class="cookie-consent__caption">We use analytics cookies to understand how the site is used. Read more in our <a href="#">privacy policy</a>.</p>
        <div class="cookie-consent__btns">
        <button class="btn btn--sm btn--outline-primary" type="button" data-action="cookie-decline">Decline</button>
          <button class="btn btn--sm btn--primary" type="button" data-action="cookie-accept">Accept</button>
        </div>
      </section></div>`;

    // At the start of the body, so it's the first thing keyboard and screen reader users reach (without stealing the focus).
    document.body.insertAdjacentHTML('afterbegin', caption);
    consentModal = document.querySelector('.cookie-consent');
  }

  function animationEndCallback () {
    consentModal.removeEventListener('animationend', animationEndCallback);

    if (redirect) {
      window.location.reload();
    }
  }

  document.addEventListener('click', (e) => {
    if (
      e.target &&
      e.target.getAttribute('data-action') === 'cookie-accept'
    ) {
      cookie.set('analytics', 'accepted', 365);

      redirect = true;
      consentModal.classList.add('cookie-consent--slideout');
      consentModal.addEventListener('animationend', animationEndCallback);
    }

    if (
      e.target &&
      e.target.getAttribute('data-action') === 'cookie-decline'
    ) {
      cookie.set('denied', 'true');

      redirect = false;
      consentModal.classList.add('cookie-consent--slideout');
      consentModal.addEventListener('animationend', animationEndCallback);
    }
  });

  btns.forEach((btn) => {
    if (
      cookie.isset(`${btn.getAttribute('data-type')}`) &&
      cookie.get(`${btn.getAttribute('data-type')}`) === 'accepted'
    ) {
      btn.innerHTML = `${btn.getAttribute('data-on-text')} ${btn.innerHTML}`;
    } else {
      btn.innerHTML = `${btn.getAttribute('data-off-text')} ${btn.innerHTML}`;
    }

    btn.addEventListener('click', () => {
      if (
        cookie.isset(`${btn.getAttribute('data-type')}`) &&
        cookie.get(`${btn.getAttribute('data-type')}`) === 'accepted'
      ) {
        cookie.remove(`${btn.getAttribute('data-type')}`);
      } else {
        cookie.set(`${btn.getAttribute('data-type')}`, 'accepted', 365);
      }
      window.location.reload();
    });
  });
})();

Cookie Conset

A minimal cookie consent banner with equal "Accept" and "Decline" choices, a category cookie that tells your code whether it may load analytics, and an opt-out button for your privacy page.

How It Works

The script differentiates two types of action at the consent banner:

  • accept all types of cookies,
  • decline them.

We place a session cookie when you decline them and ask for permission (again) next time.

If you accept them, we place the category cookie(s). In this case, demo-gdpr-analytics. We use this cookie to identify if we should load analytics-related cookies (like Google Analytics). You can set up any category.

To load your tracking codes, use the following snippet:

if (
  Cookie.isset('demo-gdpr-analytics')
  && Cookie.get('demo-gdpr-analytics') === 'accepted'
) {
  console.log('ANALYTICS');
}

Technical Details

  • As you see in the example, we also included an opt-out button; this is just a button to turn on or off any cookie type. You usually want to place it on your privacy policy page.
  • We handle everything from JS because, in some cases, the back-end cache can kill the cookies.
  • We reload the page after an opt-in or opt-out action because this is the simplest way to load (or turn off) the related scripts.
  • The banner is inserted at the start of the body as a labelled region ("Cookie consent"), so it's the first thing keyboard and screen reader users reach. It doesn't steal the focus on load: moving the focus without a user action is disorienting.
  • "Accept" and "Decline" are both one click away and equally visible; hiding the decline option behind a settings page isn't allowed under the GDPR.
  • The banner uses the surface, border and shadow tokens, so it follows the dark mode and every style. It slides in with translate, instantly with reduced motion.

Dependencies

  • Qkie, our tiny cookie helper. Change the import path in the script to where you keep it.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.