Self-Hosted Cookie Consent Banner Generator

Answer a few questions and get a bandeau cookies you own: plain HTML, CSS and vanilla JavaScript you paste into your own site. It genuinely blocks non-essential scripts until someone consents, stores the choice in localStorage, and puts “Reject all” right next to “Accept all” at the same size - the detail the CNIL actually enforces. No third-party CMP, no account, no pageview cap.

Everything is generated in your browser. The snippet you get makes no network requests of its own - no CDN, no consent-management server, no pageview counter to run out of.

Shown in the banner text - visitors have to know who is asking.

Required. Consent is not informed without it.

Cookie categories you set

Always

Strictly necessary

Exempt from consent, so it is always present and cannot be switched off.

The CNIL treats about 6 months as good practice, for a refusal just as much as for consent.

Exposed as window.cookieConsent and stored under "cookieConsent.v1".

Two things you cannot switch off here

“Reject all” is always rendered next to “Accept all”, at the same size and weight, at the first level of the banner - and optional categories are always generated unticked. Those are the two details regulators actually fine sites over, so there is deliberately no option to break them.

  • FixAdd your site or company name - the banner has to say who is asking.

Preview

We use cookies

This site uses cookies that are strictly necessary to run the site. With your permission we would also like to set optional cookies. You can accept them all, refuse them all, or choose category by category.

Accept allReject all
<style>
/* Cookie consent banner - generated by usezivo.com.
   Dependency-free: no fonts, no CDN, no network requests of its own. */

.cc-cookieconsent-root { position: fixed; z-index: 2147483000; }

.cc-cookieconsent-backdrop {
  position: fixed; inset: 0; z-index: 2147482999;
  background: rgba(15, 23, 42, 0.45);
}

.cc-cookieconsent-banner {
  position: fixed;
  left: 0; right: 0; bottom: 0; max-width: none; border-radius: 0; border-left: 0; border-right: 0; border-bottom: 0;
  box-sizing: border-box;
  padding: 18px 20px;
  background: #ffffff;
  color: #0f172a;
  border: 1px solid #e2e8f0;
  border-radius: 14px;
  box-shadow: 0 10px 40px rgba(15, 23, 42, 0.18);
  font: 400 14px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

.cc-cookieconsent-title { margin: 0 0 6px; font-size: 15px; font-weight: 700; }
.cc-cookieconsent-text { margin: 0 0 14px; color: #475569; }
.cc-cookieconsent-text a { color: #2563eb; text-decoration: underline; }

/* Accept and Reject are deliberately the SAME component: same size, same
   weight, same visual prominence, side by side at the first level. */
.cc-cookieconsent-actions { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.cc-cookieconsent-btn {
  appearance: none;
  flex: 0 0 auto;
  min-width: 148px;
  padding: 11px 18px;
  font: inherit;
  font-weight: 600;
  line-height: 1.2;
  border-radius: 10px;
  border: 1px solid #2563eb;
  background: #2563eb;
  color: #ffffff;
  cursor: pointer;
}
.cc-cookieconsent-btn:hover { filter: brightness(0.94); }
.cc-cookieconsent-btn:focus-visible { outline: 3px solid #2563eb; outline-offset: 2px; }
.cc-cookieconsent-btn--secondary { background: transparent; color: #2563eb; }
.cc-cookieconsent-link {
  appearance: none; background: none; border: 0; padding: 8px 4px;
  font: inherit; color: #475569; text-decoration: underline; cursor: pointer;
}
.cc-cookieconsent-link:hover { color: #0f172a; }

.cc-cookieconsent-panel {
  position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
  box-sizing: border-box;
  width: min(620px, calc(100vw - 32px));
  max-height: min(760px, calc(100vh - 32px));
  overflow-y: auto;
  padding: 22px;
  background: #ffffff; color: #0f172a;
  border-radius: 16px;
  box-shadow: 0 20px 60px rgba(15, 23, 42, 0.3);
  font: 400 14px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.cc-cookieconsent-panel h2 { margin: 0 0 6px; font-size: 17px; }
.cc-cookieconsent-panel-intro { margin: 0 0 16px; color: #475569; }

.cc-cookieconsent-cat {
  padding: 14px 0;
  border-top: 1px solid #e2e8f0;
}
.cc-cookieconsent-cat-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.cc-cookieconsent-cat-name { font-weight: 600; }
.cc-cookieconsent-cat-desc { margin: 6px 0 0; color: #64748b; font-size: 13px; }
.cc-cookieconsent-always { font-size: 12px; font-weight: 600; color: #16a34a; }

/* Switch */
.cc-cookieconsent-switch { position: relative; display: inline-block; width: 44px; height: 24px; flex: 0 0 auto; }
.cc-cookieconsent-switch input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.cc-cookieconsent-slider {
  position: absolute; inset: 0; border-radius: 999px;
  background: #cbd5e1; transition: background 0.15s ease; pointer-events: none;
}
.cc-cookieconsent-slider::after {
  content: ""; position: absolute; top: 3px; left: 3px;
  width: 18px; height: 18px; border-radius: 50%; background: #fff;
  transition: transform 0.15s ease;
}
.cc-cookieconsent-switch input:checked + .cc-cookieconsent-slider { background: #2563eb; }
.cc-cookieconsent-switch input:checked + .cc-cookieconsent-slider::after { transform: translateX(20px); }
.cc-cookieconsent-switch input:focus-visible + .cc-cookieconsent-slider { outline: 3px solid #2563eb; outline-offset: 2px; }

.cc-cookieconsent-panel-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 18px; padding-top: 16px; border-top: 1px solid #e2e8f0; }

@media (max-width: 520px) {
  .cc-cookieconsent-banner { left: 8px; right: 8px; bottom: 8px; max-width: none; border-radius: 14px; border: 1px solid #e2e8f0; transform: none; top: auto; }
  .cc-cookieconsent-btn { flex: 1 1 100%; min-width: 0; }
}

@media (prefers-color-scheme: dark) {
  .cc-cookieconsent-banner, .cc-cookieconsent-panel { background: #0f172a; color: #e2e8f0; border-color: #334155; }
  .cc-cookieconsent-text, .cc-cookieconsent-panel-intro, .cc-cookieconsent-cat-desc { color: #94a3b8; }
  .cc-cookieconsent-cat, .cc-cookieconsent-panel-actions { border-color: #334155; }
  .cc-cookieconsent-link { color: #94a3b8; }
  .cc-cookieconsent-link:hover { color: #e2e8f0; }
  .cc-cookieconsent-slider { background: #475569; }
  @media (max-width: 520px) { .cc-cookieconsent-banner { border-color: #334155; } }
}
</style>

<!-- Cookie consent banner - generated by usezivo.com. Paste before </body>. -->
<div id="cc-cookieconsent-banner" class="cc-cookieconsent-banner" role="dialog" aria-modal="false" aria-labelledby="cc-cookieconsent-title" hidden>
  <p class="cc-cookieconsent-title" id="cc-cookieconsent-title">We use cookies</p>
  <p class="cc-cookieconsent-text">This site uses cookies that are strictly necessary to run the site. With your permission we would also like to set optional cookies. You can accept them all, refuse them all, or choose category by category. <a href="/privacy">Privacy policy</a></p>
  <div class="cc-cookieconsent-actions">
    <button type="button" class="cc-cookieconsent-btn" data-cc-cookieconsent-action="accept">Accept all</button>
    <button type="button" class="cc-cookieconsent-btn" data-cc-cookieconsent-action="reject">Reject all</button>
    <button type="button" class="cc-cookieconsent-link" data-cc-cookieconsent-action="open">Customise</button>
  </div>
</div>

<div id="cc-cookieconsent-panel" class="cc-cookieconsent-panel" role="dialog" aria-modal="true" aria-labelledby="cc-cookieconsent-panel-title" hidden>
  <h2 id="cc-cookieconsent-panel-title">Cookie preferences</h2>
  <p class="cc-cookieconsent-panel-intro">Strictly necessary cookies cannot be switched off. Every other category is off until you turn it on.</p>
      <div class="cc-cookieconsent-cat">
        <div class="cc-cookieconsent-cat-head">
          <span class="cc-cookieconsent-cat-name">Strictly necessary</span>
          <span class="cc-cookieconsent-always">Always active</span>
        </div>
        <p class="cc-cookieconsent-cat-desc">Needed for the site to work: security, load balancing, remembering what is in your basket and storing this cookie choice. These are exempt from consent.</p>
      </div>
      <div class="cc-cookieconsent-cat">
        <div class="cc-cookieconsent-cat-head">
          <span class="cc-cookieconsent-cat-name">Analytics</span>
          <label class="cc-cookieconsent-switch">
              <input type="checkbox" data-cc-cookieconsent-cat="analytics" aria-label="Analytics">
              <span class="cc-cookieconsent-slider"></span>
            </label>
        </div>
        <p class="cc-cookieconsent-cat-desc">Help us understand which pages are visited and where people get stuck, so we can improve the site. Measurement only, never used to target advertising.</p>
      </div>
  <div class="cc-cookieconsent-panel-actions">
    <button type="button" class="cc-cookieconsent-btn" data-cc-cookieconsent-action="save">Save my choices</button>
    <button type="button" class="cc-cookieconsent-btn cc-cookieconsent-btn--secondary" data-cc-cookieconsent-action="accept">Accept all</button>
    <button type="button" class="cc-cookieconsent-btn cc-cookieconsent-btn--secondary" data-cc-cookieconsent-action="reject">Reject all</button>
  </div>
</div>

<script>
/* Cookie consent - generated by usezivo.com. Vanilla JS, no dependencies,
   no network requests. Paste after the markup, or load as an external file. */
(function () {
  "use strict";

  var KEY = "cookieConsent.v1";
  var MAX_AGE_MS = 183 * 24 * 60 * 60 * 1000;
  var OPTIONAL = ["analytics"];
  var PREFIX = "cc-cookieconsent";

  var banner = document.getElementById(PREFIX + "-banner");
  var panel = document.getElementById(PREFIX + "-panel");
  var backdrop = null;
  var lastFocus = null;

  // --- storage ------------------------------------------------------------
  // localStorage can throw outright in some privacy modes, so every access is
  // guarded. If it is unavailable the banner simply reappears next visit,
  // which is the safe direction to fail in: nothing gets unblocked.
  function read() {
    try {
      var raw = localStorage.getItem(KEY);
      if (!raw) return null;
      var saved = JSON.parse(raw);
      if (!saved || typeof saved !== "object") return null;
      if (typeof saved.at !== "number" || Date.now() - saved.at > MAX_AGE_MS) return null;
      var out = { necessary: true };
      for (var i = 0; i < OPTIONAL.length; i++) {
        out[OPTIONAL[i]] = saved.categories && saved.categories[OPTIONAL[i]] === true;
      }
      return out;
    } catch (e) {
      return null;
    }
  }

  function write(categories) {
    try {
      localStorage.setItem(KEY, JSON.stringify({ v: 1, at: Date.now(), categories: categories }));
    } catch (e) {
      /* storage blocked - the choice applies to this page load only */
    }
  }

  var state = read();

  // --- script gating ------------------------------------------------------
  // Third-party tags are written as:
  //   <script type="text/plain" data-cookie-category="analytics" src="..."></script>
  // The browser will not execute type="text/plain", so nothing runs until the
  // matching category is granted, at which point the tag is swapped for a real
  // one. Inline tags work the same way.
  function unblock(granted) {
    var pending = document.querySelectorAll('script[type="text/plain"][data-cookie-category]');
    for (var i = 0; i < pending.length; i++) {
      var node = pending[i];
      var category = node.getAttribute("data-cookie-category");
      if (!granted[category]) continue;
      var real = document.createElement("script");
      for (var a = 0; a < node.attributes.length; a++) {
        var attr = node.attributes[a];
        if (attr.name === "type" || attr.name === "data-cookie-category") continue;
        real.setAttribute(attr.name, attr.value);
      }
      real.type = "text/javascript";
      if (!node.src) real.text = node.text;
      node.parentNode.replaceChild(real, node);
    }
  }

  var listeners = [];
  function emit(granted) {
    unblock(granted);
    for (var i = 0; i < listeners.length; i++) {
      try {
        listeners[i](granted);
      } catch (e) {
        /* one bad listener must not break the rest */
      }
    }
    try {
      document.dispatchEvent(new CustomEvent("cookieConsent:change", { detail: granted }));
    } catch (e) {
      /* CustomEvent unsupported - the callback API still works */
    }
  }

  // --- UI -----------------------------------------------------------------
  function showBanner() {
    if (!banner) return;
    banner.hidden = false;
    /* no backdrop configured */
  }

  function hideBanner() {
    if (banner) banner.hidden = true;
    if (backdrop && backdrop.parentNode) {
      backdrop.parentNode.removeChild(backdrop);
      backdrop = null;
    }
  }

  function boxes() {
    return panel ? panel.querySelectorAll("[data-" + PREFIX + "-cat]") : [];
  }

  function openPanel() {
    if (!panel) return;
    lastFocus = document.activeElement;
    // Reflect the stored choice, or leave every optional box OFF when there is
    // no stored choice yet. Nothing is ever pre-ticked.
    var current = read();
    var list = boxes();
    for (var i = 0; i < list.length; i++) {
      var id = list[i].getAttribute("data-" + PREFIX + "-cat");
      list[i].checked = !!(current && current[id]);
    }
    panel.hidden = false;
    var first = panel.querySelector("button, input");
    if (first) first.focus();
    document.addEventListener("keydown", onKeydown);
  }

  function closePanel() {
    if (!panel) return;
    panel.hidden = true;
    document.removeEventListener("keydown", onKeydown);
    if (lastFocus && lastFocus.focus) lastFocus.focus();
  }

  function onKeydown(e) {
    // Escape closes the PREFERENCES panel only. It never dismisses the banner:
    // dismissing without choosing must not be treated as consent.
    if (e.key === "Escape") closePanel();
  }

  function decide(categories) {
    var granted = { necessary: true };
    for (var i = 0; i < OPTIONAL.length; i++) {
      granted[OPTIONAL[i]] = categories[OPTIONAL[i]] === true;
    }
    state = granted;
    write(granted);
    closePanel();
    hideBanner();
    emit(granted);
  }

  function all(value) {
    var out = {};
    for (var i = 0; i < OPTIONAL.length; i++) out[OPTIONAL[i]] = value;
    return out;
  }

  function fromPanel() {
    var out = {};
    var list = boxes();
    for (var i = 0; i < list.length; i++) {
      out[list[i].getAttribute("data-" + PREFIX + "-cat")] = list[i].checked === true;
    }
    return out;
  }

  document.addEventListener("click", function (e) {
    var target = e.target.closest ? e.target.closest("[data-" + PREFIX + "-action]") : null;
    if (!target) return;
    var action = target.getAttribute("data-" + PREFIX + "-action");
    if (action === "accept") decide(all(true));
    else if (action === "reject") decide(all(false));
    else if (action === "save") decide(fromPanel());
    else if (action === "open") openPanel();
    else if (action === "close") closePanel();
  });

  // --- public API ---------------------------------------------------------
  window["cookieConsent"] = {
    /** true if that category has been granted. Always true for "necessary". */
    has: function (category) {
      if (category === "necessary") return true;
      return !!(state && state[category]);
    },
    /** The full granted map, or null if no choice has been made yet. */
    get: function () {
      return state ? JSON.parse(JSON.stringify(state)) : null;
    },
    /** Run cb now (if a choice exists) and on every later change. Returns an unsubscribe fn. */
    onChange: function (cb) {
      listeners.push(cb);
      if (state) {
        try {
          cb(state);
        } catch (e) {
          /* ignore */
        }
      }
      return function () {
        var i = listeners.indexOf(cb);
        if (i > -1) listeners.splice(i, 1);
      };
    },
    /** Wire this to a permanent "Cookie preferences" link in your footer. */
    openPreferences: openPanel,
    /** Forget the stored choice and ask again. Withdrawal must be as easy as consent. */
    reset: function () {
      try {
        localStorage.removeItem(KEY);
      } catch (e) {
        /* ignore */
      }
      state = null;
      showBanner();
    },
  };

  // --- boot ---------------------------------------------------------------
  if (state) {
    // A stored choice: apply it silently, do not nag.
    emit(state);
  } else {
    // No choice yet -> nothing is unblocked, and the banner asks.
    showBanner();
  }
})();
</script>

The snippet updates live, but fix the items above before shipping it.

Where the rules above come from

This generator produces the banner UI and the consent-storage mechanism. It is not legal advice and cannot by itself make a site compliant - what actually matters is which trackers you run, whether you have genuinely wired them through the gate, and what your privacy policy says. Rules also differ between countries and change over time. If the stakes are high, have a lawyer look at it.

Why self-hosted instead of Axeptio or Cookiebot

Hosted consent platforms are excellent products, but their free tiers are metered: cross a monthly pageview threshold and the banner either stops working or the bill starts. They also load their own JavaScript from their own domain on every single page view, which is a third-party request added to your site in the name of privacy, and a dependency that can change under you.

The snippet this tool writes is about 6 KB, has no dependencies, makes no network requests of its own, and lives in your repository where you can read every line of it. There is no vendor to run out of quota with, and nothing phones home.

What the banner doesHow
Blocks tags before consentThird-party tags are written as type="text/plain" with a data-cookie-category attribute, which browsers refuse to execute. On consent the tag is replaced with a real script element and only then runs.
Refusal as easy as acceptanceAccept all and Reject all are the same button component - same width, padding, weight and colour - side by side at the first level.
Nothing pre-tickedThe generated preference switches carry no checked attribute. Ever.
Silence is not consentDismissing or ignoring the banner stores nothing and unblocks nothing. Only an explicit click writes a choice.
Withdrawal on demandwindow.<namespace>.openPreferences() reopens the panel, and .reset() forgets the choice - wire either to a footer link.
Remembers the answerlocalStorage, with a timestamp and an expiry you choose (6 months by default), so visitors are not re-asked page after page.

Häufig gestellte Fragen

Comment créer un bandeau cookies conforme CNIL gratuit sans Axeptio ?

Fill in your site name, your privacy policy link and the cookie categories you actually use, then copy the generated block into your page before the closing body tag. You get vanilla HTML, CSS and JavaScript with no dependency on Axeptio, Cookiebot, Tarteaucitron or any other consent platform - so there is no free-tier pageview limit to hit and no third-party script loading on every page.

Does it really block Google Analytics before consent, or just hide the banner?

It really blocks it, but you have to do one thing: change your tag from type="text/javascript" to type="text/plain" and add data-cookie-category="analytics". A browser will not execute a text/plain script, so the tag sits inert in the page. The moment the visitor grants that category, the script is cloned into a real script element and runs. If you leave your tags as ordinary scripts, they will fire before the banner even appears - which is exactly the failure regulators look for. The "How to gate scripts" tab shows the exact markup.

Why is there no option to hide the Reject all button?

Because that is the single most-cited problem with non-compliant banners. The CNIL's position is that it must be as easy to refuse trackers as to accept them, and it treats a "Refuse all" button at the first level, at the same level and with the same appearance as "Accept all", as the clear and simple way to do that. This generator always emits both buttons from the same CSS class, so they are identical in size and weight. Optional categories are likewise always generated unticked, because consent needs a clear affirmative act.

Does this make my site legally compliant with the GDPR?

No, and be careful of any tool that claims otherwise. This generates the banner UI and the consent-storage mechanism. Compliance also depends on which trackers you actually run, whether you have genuinely routed each one through the gate, what your privacy and cookie policies say, how you handle data subject requests, and rules that vary by country and change over time. Treat this as a solid, honest starting point that you or your lawyer then check against your real setup.

How long is the visitor's choice remembered?

Six months by default, which the CNIL describes as good practice for storing the choice - and that applies to a refusal just as much as to consent, so someone who says no is not asked again on the next page. You can pick 30 days up to 13 months. The choice is stored in localStorage with a timestamp; once it expires the banner reappears and nothing is unblocked until the visitor answers again.

What if the visitor has localStorage disabled?

Every storage access in the generated script is wrapped in a try/catch, so a private window or a browser with site data blocked will not throw an error. The banner simply cannot remember the answer, so it reappears on the next page load and non-essential scripts stay blocked. It fails in the safe direction: no storage means no consent recorded, which means nothing extra loads.

Ähnliche Tools