Legal & Compliance

Free Cookie Consent Banner Widget

A lightweight consent banner that respects your visitors.

A clean cookie notice with accept and decline actions that remembers the visitor's choice. Links to your privacy policy and stays out of the way once answered. A simple, honest baseline for privacy compliance on content and small-business sites.

Live demo

Embed this widget

Paste either snippet into your page — a Custom HTML block on WordPress, Shopify, or Squarespace works too. Both snippets update live as you customize, so what you copy is exactly what you configured.

Hosted (recommended)html
<script src="https://hayneslab.dev/widgets/cookie-consent.js" data-hl-message="We use cookies to understand how visitors use this site and to improve your experience." data-hl-policy-link="https://hayneslab.dev/privacy" async></script>
<!-- Free Cookie Consent Banner widget by haynes lab — https://hayneslab.dev/tools/widgets/cookie-consent -->
Inline (self-contained)html
<!-- Free Cookie Consent Banner widget by haynes lab — https://hayneslab.dev/tools/widgets/cookie-consent -->
<script data-hl-message="We use cookies to understand how visitors use this site and to improve your experience." data-hl-policy-link="https://hayneslab.dev/privacy">
/*!
 * haynes lab — Cookie Consent Banner widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/cookie-consent
 */
(function () {
  'use strict';

  var STORAGE_KEY = 'hlw_cookie-consent_choice';
  var STYLE_ID = 'hlw-cookie-consent-styles';
  var ROOT_CLASS = 'hlw-cookie-consent';

  var script = document.currentScript;
  if (!script) {
    script = document.querySelector('script[src*="widgets/cookie-consent.js"]');
  }
  if (!script) {
    console.warn('[hlw-cookie-consent] Could not locate its own <script> tag; banner not rendered.');
    return;
  }

  function attr(name, fallback) {
    var v = script.getAttribute('data-hl-' + name);
    return v === null ? fallback : v;
  }

  var config = {
    message: attr('message', 'This site uses cookies to improve your experience.'),
    policyLink: attr('policy-link', ''),
    acceptText: attr('accept-text', 'Accept'),
    declineText: attr('decline-text', 'Decline'),
    position: attr('position', 'bottom'),
    bg: attr('bg', '#141010'),
    accent: attr('accent', '#f97316')
  };

  function getChoice() {
    try {
      return window.localStorage.getItem(STORAGE_KEY);
    } catch (e) {
      return null; // storage unavailable (private mode etc.) — show the banner
    }
  }

  function setChoice(choice) {
    try {
      window.localStorage.setItem(STORAGE_KEY, choice);
    } catch (e) {
      console.warn('[hlw-cookie-consent] Could not persist consent choice (storage unavailable).');
    }
  }

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + ROOT_CLASS + ', .' + ROOT_CLASS + ' * { box-sizing: border-box; margin: 0; padding: 0; }' +
      '.' + ROOT_CLASS + ' { position: fixed; z-index: 2147483000; font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.5; color: #f5f3f0; background: var(--hlw-cc-bg, #141010); box-shadow: 0 -2px 16px rgba(0,0,0,0.18); }' +
      '.' + ROOT_CLASS + '--bottom { left: 0; right: 0; bottom: 0; }' +
      '.' + ROOT_CLASS + '--bottom-left { left: 16px; bottom: 16px; max-width: 380px; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.28); }' +
      '.' + ROOT_CLASS + '__inner { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding: 14px 18px; }' +
      '.' + ROOT_CLASS + '--bottom-left .' + ROOT_CLASS + '__inner { flex-direction: column; align-items: stretch; }' +
      '.' + ROOT_CLASS + '__text { flex: 1 1 280px; }' +
      '.' + ROOT_CLASS + '__link { color: inherit; text-decoration: underline; text-underline-offset: 2px; opacity: 0.85; }' +
      '.' + ROOT_CLASS + '__link:hover { opacity: 1; }' +
      '.' + ROOT_CLASS + '__actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }' +
      '.' + ROOT_CLASS + '__btn { font: inherit; font-weight: 600; font-size: 13px; line-height: 1; padding: 10px 18px; border: 0; border-radius: 8px; cursor: pointer; white-space: nowrap; }' +
      '.' + ROOT_CLASS + '__btn--accept { background: var(--hlw-cc-accent, #f97316); color: #ffffff; }' +
      '.' + ROOT_CLASS + '__btn--accept:hover { filter: brightness(1.1); }' +
      '.' + ROOT_CLASS + '__btn--decline { background: transparent; color: inherit; border: 1px solid rgba(255,255,255,0.35); }' +
      '.' + ROOT_CLASS + '__btn--decline:hover { border-color: rgba(255,255,255,0.7); }' +
      '.' + ROOT_CLASS + '__btn:focus-visible, .' + ROOT_CLASS + ' a:focus-visible { outline: 2px solid #ffffff; outline-offset: 2px; }' +
      '.' + ROOT_CLASS + '__credit { font-size: 11px; opacity: 0.55; }' +
      '.' + ROOT_CLASS + '--bottom .' + ROOT_CLASS + '__credit { position: absolute; right: 8px; bottom: 4px; }' +
      '.' + ROOT_CLASS + '--bottom-left .' + ROOT_CLASS + '__credit { text-align: right; padding: 0 18px 8px; }' +
      '.' + ROOT_CLASS + '__credit a { display: inline-flex; align-items: center; gap: 4px; color: inherit; text-decoration: none; }' +
      '.' + ROOT_CLASS + '__credit a svg { display: block; }' +
      '.' + ROOT_CLASS + '__credit a:hover { text-decoration: underline; }' +
      '@media (max-width: 560px) { .' + ROOT_CLASS + '__inner { flex-direction: column; align-items: stretch; } .' + ROOT_CLASS + '__actions { justify-content: stretch; } .' + ROOT_CLASS + '__btn { flex: 1; } }';
    var style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }

  function el(tag, className, text) {
    var node = document.createElement(tag);
    if (className) node.className = className;
    if (text !== undefined) node.textContent = text;
    return node;
  }

  function logoSvg() {
    var svgNS = 'http://www.w3.org/2000/svg';
    var svg = document.createElementNS(svgNS, 'svg');
    svg.setAttribute('viewBox', '0 0 32 32');
    svg.setAttribute('width', '13');
    svg.setAttribute('height', '13');
    svg.setAttribute('aria-hidden', 'true');
    svg.setAttribute('focusable', 'false');
    function circle(cx, cy, r, attrs) {
      var c = document.createElementNS(svgNS, 'circle');
      c.setAttribute('cx', cx);
      c.setAttribute('cy', cy);
      c.setAttribute('r', r);
      for (var key in attrs) c.setAttribute(key, attrs[key]);
      svg.appendChild(c);
    }
    circle(16, 16, 16, { fill: '#0f0a0a' });
    circle(16, 16, 12, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.3 });
    circle(16, 16, 9, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.5 });
    circle(16, 16, 6, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1.2, opacity: 0.9 });
    circle(16, 16, 3, { fill: '#06b6d4' });
    circle(24.5, 7.5, 2, { fill: '#f97316' });
    circle(7, 16, 1.8, { fill: '#f97316' });
    circle(20, 20, 1.5, { fill: '#f97316' });
    return svg;
  }

  function render() {
    // The script may run again (live demo re-injection, double embed) —
    // remove any previous instance so banners never stack.
    var oldRoots = document.querySelectorAll('.' + ROOT_CLASS);
    for (var i = 0; i < oldRoots.length; i++) {
      oldRoots[i].parentNode.removeChild(oldRoots[i]);
    }

    if (getChoice()) return; // visitor already answered

    injectStyles();

    var floating = config.position === 'bottom-left';
    var root = el('div', ROOT_CLASS + ' ' + ROOT_CLASS + (floating ? '--bottom-left' : '--bottom'));
    root.setAttribute('role', 'region');
    root.setAttribute('aria-label', 'Cookie consent notice');
    root.style.setProperty('--hlw-cc-bg', config.bg);
    root.style.setProperty('--hlw-cc-accent', config.accent);

    var inner = el('div', ROOT_CLASS + '__inner');
    var text = el('p', ROOT_CLASS + '__text');
    text.appendChild(document.createTextNode(config.message + ' '));
    if (config.policyLink) {
      var link = el('a', ROOT_CLASS + '__link', 'Learn more');
      link.href = config.policyLink;
      link.target = '_blank';
      link.rel = 'noopener';
      text.appendChild(link);
    }
    inner.appendChild(text);

    var actions = el('div', ROOT_CLASS + '__actions');
    if (config.declineText !== '') {
      var declineBtn = el('button', ROOT_CLASS + '__btn ' + ROOT_CLASS + '__btn--decline', config.declineText);
      declineBtn.type = 'button';
      declineBtn.setAttribute('aria-label', 'Decline cookies');
      declineBtn.addEventListener('click', function () { dismiss('declined'); });
      actions.appendChild(declineBtn);
    }
    var acceptBtn = el('button', ROOT_CLASS + '__btn ' + ROOT_CLASS + '__btn--accept', config.acceptText);
    acceptBtn.type = 'button';
    acceptBtn.setAttribute('aria-label', 'Accept cookies');
    acceptBtn.addEventListener('click', function () { dismiss('accepted'); });
    actions.appendChild(acceptBtn);
    inner.appendChild(actions);
    root.appendChild(inner);

    var credit = el('div', ROOT_CLASS + '__credit');
    var creditLink = el('a');
    creditLink.appendChild(logoSvg());
    creditLink.appendChild(document.createTextNode('Powered by haynes lab'));
    creditLink.href = 'https://hayneslab.dev/tools/widgets/cookie-consent';
    creditLink.target = '_blank';
    creditLink.rel = 'noopener';
    credit.appendChild(creditLink);
    root.appendChild(credit);

    function dismiss(choice) {
      setChoice(choice);
      if (root.parentNode) root.parentNode.removeChild(root);
    }

    document.body.appendChild(root);
  }

  if (document.body) {
    render();
  } else {
    document.addEventListener('DOMContentLoaded', render);
  }
})();

</script>

Configuration

Every option is an attribute on the script tag — the same attributes the customizer above exposes. All attributes are optional unless marked required — defaults apply when omitted.

AttributeDefaultDescription
data-hl-message"This site uses cookies to improve your experience."Notice text shown in the banner. Plain text, one or two sentences.
data-hl-policy-link(none)URL of your privacy/cookie policy. When set, a "Learn more" link appears in the banner.
data-hl-accept-text"Accept"Label of the accept button.
data-hl-decline-text"Decline"Label of the decline button. Set to "" to hide the decline action.
data-hl-position"bottom"Where the banner sits: "bottom" (full-width bar) or "bottom-left" (floating card).
data-hl-bg"#141010"Any CSS color for the banner background.
data-hl-accent"#f97316"Any CSS color for the accept button.

Frequently asked questions

How do I install the cookie consent banner?

Paste the embed snippet before your closing </body> tag and set data-hl-policy-link to your privacy policy URL. The banner appears for first-time visitors and remembers their choice in localStorage.

How do I customize the wording and layout?

Use data-hl-message for the notice text, data-hl-accept-text and data-hl-decline-text for the buttons, and data-hl-position to switch between a full-width bottom bar and a floating card. Colors come from data-hl-bg and data-hl-accent.

Does it work on WordPress, Shopify, or Squarespace?

Yes — it is one dependency-free vanilla JS file. Note that this is a notice banner, not a consent-management platform: it informs visitors and stores their choice, but blocking third-party cookies until consent still depends on how those scripts are loaded on your site.

More free widgets

Need a custom widget?

We build custom widgets, integrations, and whole products. Tell us what your site needs and we'll scope it.

Get in Touch