自托管 Cookie 同意横幅生成器

回答几个问题,即可获得一个真正属于你自己的 Cookie 同意横幅:纯 HTML、CSS 和原生 JavaScript,直接粘贴到你自己的网站即可。它会在用户同意之前真正阻止非必要脚本运行,将选择保存在 localStorage 中,并把「全部拒绝」按钮做得和「全部接受」一样醒目——这正是法国 CNIL 等监管机构明确要求的细节。无需任何第三方同意平台,无需账号,没有页面浏览量上限。

一切都在你的浏览器中生成。你得到的代码片段本身不会发起任何网络请求——没有 CDN,没有同意管理服务器,也没有会用完的页面浏览计数器。

会显示在横幅文案中——访客需要知道是谁在征求同意。

必填。没有它,同意就不算是知情同意。

你设置的 Cookie 类别

始终

Strictly necessary

豁免于同意要求,因此始终存在且无法关闭。

法国 CNIL 认为约 6 个月是良好做法,无论是拒绝还是同意。

以 window.cookieConsent 形式暴露,并存储在 "cookieConsent.v1" 下。

这里有两项你无法关闭的设置

在横幅的第一层,"拒绝全部"始终与"接受全部"以相同的大小和字重并排显示——可选类别也始终以未勾选状态生成。这两个细节正是监管机构真正会据以处罚网站的地方,因此这里故意不提供破坏它们的选项。

  • 需修复Add your site or company name - the banner has to say who is asking.

预览

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>

代码片段会实时更新,但在上线前请先修复上面列出的问题。

以上规则的依据来源

此生成器仅生成横幅界面和同意状态的存储机制。它不构成法律建议,也无法单凭自身使网站合规——真正重要的是你运行了哪些追踪脚本、是否真的将它们接入了这个门控,以及你的隐私政策写了什么。规则也因国家而异,并会随时间变化。如果风险较高,请让律师审查。

为什么自行托管而不是使用 Axeptio 或 Cookiebot

托管式同意管理平台都是优秀的产品,但它们的免费套餐是按量计费的:一旦超过每月页面浏览量门槛,横幅就会停止工作,或者开始收费。它们还会在每次页面浏览时从自己的域名加载自己的 JavaScript,这等于是以隐私的名义给你的网站添加了一个第三方请求,也是一个可能在你无法控制的情况下发生变化的依赖项。

本工具生成的代码片段大约只有 6 KB,没有任何依赖,自身不会发出任何网络请求,并且就放在你自己的代码仓库里,你可以逐行阅读。没有任何供应商的配额会被用完,也没有任何东西会偷偷向外部服务器发送数据。

横幅做什么如何实现
在获得同意之前拦截标签第三方标签被写成 type="text/plain" 并带有 data-cookie-category 属性,浏览器会拒绝执行它。用户同意后,该标签会被替换为真正的 script 元素,也只有到那时才会运行。
拒绝和同意一样简单“全部接受”和“全部拒绝”是同一个按钮组件——宽度、内边距、字重和颜色完全相同——在第一层并排显示。
没有任何选项被预先勾选生成的偏好开关从不带有 checked 属性。绝不。
沉默不等于同意关闭或忽略横幅不会存储任何东西,也不会解锁任何东西。只有明确点击才会写入一个选择。
随时可以撤回同意<ns/>.openPreferences() 会重新打开面板,而 .reset() 会清除已记录的选择——可以把其中任意一个绑定到页脚链接上。
会记住用户的回答使用 localStorage 存储,带有时间戳和你自行设定的有效期(默认为 6 个月),这样访客就不会在每一页都被重复询问。

常见问题

不使用付费平台,如何免费创建一个合规的 Cookie 横幅?

填写你的网站名称、隐私政策链接以及你实际使用的 Cookie 类别,然后把生成的代码块复制粘贴到页面的 body 闭合标签之前。你得到的是不依赖 Axeptio、Cookiebot、Tarteaucitron 或任何其他同意管理平台的纯 HTML、CSS 和 JavaScript——因此不会碰到免费套餐的页面浏览量上限,也不会在每个页面加载第三方脚本。

它在获得同意之前真的会阻止 Google Analytics 吗,还是只是隐藏横幅?

它确实会真正阻止,但你需要做一件事:把标签的 type 从 "text/javascript" 改为 "text/plain",并添加 data-cookie-category="analytics"。浏览器不会执行 text/plain 类型的脚本,所以该标签会在页面中保持惰性。一旦访客授予该类别权限,脚本就会被克隆为真正的 script 元素并运行。

为什么没有隐藏「全部拒绝」按钮的选项?

因为这是不合规横幅中被提及最多的问题。CNIL 的立场是拒绝追踪器必须和接受一样容易,并认为在第一层级设置一个与「全部接受」外观相同的「全部拒绝」按钮,是实现这一点的明确方式。这个生成器始终用同一个 CSS 类生成这两个按钮,因此它们在大小和粗细上完全一致。

这能让我的网站在法律上符合 GDPR 吗?

不能,对任何声称可以做到这一点的工具都要保持警惕。这里生成的只是横幅界面和同意存储机制。合规与否还取决于你实际运行了哪些追踪器、每一个是否真正经过了这道关卡、你的隐私政策和 Cookie 政策写了什么,以及因国家而异且随时间变化的法规。

相关工具