Skip to content
UI ComponentCookie ConsentOverlayNew

Cookie Consent Banner with Preference Center

A GDPR-style cookie consent solution: a friendly banner with Accept all / Reject all / Customize, a preference center with category switches and expandable cookie tables, Global Privacy Control support, consent stored for 180 days with a version you can bump, scripts that only run after their category is allowed, a floating cookie button to change your mind and a small cookieConsent API plus a change event for the rest of your site.

  • Published 15 Oct 2026
  • 10 views
Cookie Consent Banner with Preference Center — preview

Overview

Cookie Consent Banner with Preference Center

Cookies, but honest — a consent banner with a real preference center that actually blocks scripts until people say yes.


What's Included

  • Banner with Accept all, Reject all and Customize
  • Preference center with switches and expandable cookie tables
  • Blocks <script type="text/plain" data-consent> until allowed
  • Respects Global Privacy Control (marketing off by default)
  • Consent saved for 180 days with a version number
  • Floating 🍪 button to reopen preferences
  • window.cookieConsent API + cookieconsent:change event

Dependencies

Already linked inside the component's files — no extra setup needed.


Desktop Preview


HTML

html
1<!doctype html>
2<html lang="en">
3  <head>
4    <meta charset="UTF-8" />
5    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6    <title>Cookie Consent with Preferences | MetaSkillGroup</title>
7    <link rel="stylesheet" href="style.css" />
8
9    <!-- Scripts like these only run after the visitor allows their category -->
10    <script type="text/plain" data-consent="analytics">window.consentLog?.("📊 Analytics script loaded");</script>
11    <script type="text/plain" data-consent="marketing">window.consentLog?.("🎯 Marketing pixel loaded");</script>
12    <script type="text/plain" data-consent="functional">window.consentLog?.("💬 Live chat loaded");</script>
13  </head>
14  <body>
15    <!-- created by metaskillgroup-team -->
16
17    <!-- Demo page -->
18    <main class="demo">
19      <span class="eyebrow">Cookie consent</span>
20      <h1>Cookies, but <em>honest.</em></h1>
21      <p>A consent banner with a real preference center. Scripts stay switched off until the visitor allows their category — and the choice is remembered for 180 days.</p>
22
23      <section class="panel" aria-label="Consent status">
24        <div class="panel-head">
25          <b>Live consent state</b>
26          <div class="panel-actions">
27            <button type="button" data-open-prefs>Preferences</button>
28            <button type="button" id="reset">Reset</button>
29          </div>
30        </div>
31        <ul class="states" id="states"></ul>
32        <ol class="log" id="log"><li>Waiting for a choice…</li></ol>
33      </section>
34    </main>
35
36    <!-- Banner -->
37    <section class="banner" id="banner" role="region" aria-label="Cookie consent" hidden>
38      <div class="cookie-art" aria-hidden="true">🍪</div>
39      <div class="banner-text">
40        <h2>We value your privacy</h2>
41        <p>We use cookies to run the site, understand what you like and show relevant offers. You choose what's allowed. <a href="#">Cookie policy</a></p>
42        <p class="gpc-note" id="gpc-note" hidden>🛡 Your browser sends <b>Global Privacy Control</b>, so marketing stays off by default.</p>
43      </div>
44      <div class="banner-actions">
45        <button type="button" class="btn ghost" data-open-prefs>Customize</button>
46        <button type="button" class="btn outline" data-reject>Reject all</button>
47        <button type="button" class="btn solid" data-accept>Accept all</button>
48      </div>
49    </section>
50
51    <!-- Preference center -->
52    <dialog class="prefs" id="prefs" aria-labelledby="prefs-title">
53      <form method="dialog" class="prefs-box" id="prefs-form">
54        <header class="prefs-head">
55          <h2 id="prefs-title">Cookie preferences</h2>
56          <button type="button" class="close" id="prefs-close" aria-label="Close">✕</button>
57        </header>
58        <p class="prefs-intro">Choose which cookies you allow. Strictly necessary cookies can't be turned off — the site wouldn't work without them.</p>
59        <div class="cats" id="cats"></div>
60        <footer class="prefs-foot">
61          <button type="button" class="btn outline" data-reject>Reject all</button>
62          <div>
63            <button type="button" class="btn ghost" id="save">Save choices</button>
64            <button type="button" class="btn solid" data-accept>Accept all</button>
65          </div>
66        </footer>
67      </form>
68    </dialog>
69
70    <!-- Reopen button (after a choice is made) -->
71    <button type="button" class="reopen" id="reopen" aria-label="Cookie preferences" hidden>🍪</button>
72
73    <div class="toast" id="toast" role="status" aria-live="polite"></div>
74
75    <script src="script.js"></script>
76  </body>
77</html>

CSS

css
1/* Importing Google Fonts - Karla + Fraunces */
2@import url("https://fonts.googleapis.com/css2?family=Karla:wght@400;500;600;700;800&family=Fraunces:ital,opsz,wght@1,9..144,600&display=swap");
3
4* {
5  margin: 0;
6  padding: 0;
7  box-sizing: border-box;
8  font-family: "Karla", sans-serif;
9}
10
11:root {
12  --bg: #faf5ee;
13  --card: #fffdf9;
14  --text: #2b1d14;
15  --muted: #7a6758;
16  --border: #eadfd2;
17  --accent: #c2410c;
18  --accent-soft: #fdeee3;
19  --on: #15803d;
20  --ease: cubic-bezier(0.2, 0.8, 0.2, 1);
21}
22
23body {
24  min-height: 100vh;
25  background: var(--bg);
26  color: var(--text);
27}
28
29[hidden] {
30  display: none !important;
31}
32
33a {
34  color: var(--accent);
35  font-weight: 700;
36}
37
38/* ---------- Demo page ---------- */
39.demo {
40  max-width: 760px;
41  margin: 0 auto;
42  padding: 80px 20px 260px;
43}
44
45.eyebrow {
46  color: var(--accent);
47  font-size: 0.82rem;
48  font-weight: 800;
49  letter-spacing: 0.14em;
50  text-transform: uppercase;
51}
52
53.demo h1 {
54  margin: 12px 0;
55  font-size: clamp(2.4rem, 6vw, 3.8rem);
56  font-weight: 800;
57  letter-spacing: -0.03em;
58}
59
60.demo h1 em {
61  font-family: "Fraunces", serif;
62  color: var(--accent);
63}
64
65.demo > p {
66  color: var(--muted);
67  font-size: 1.08rem;
68  line-height: 1.65;
69}
70
71.panel {
72  margin-top: 34px;
73  padding: 20px;
74  border: 1px solid var(--border);
75  border-radius: 20px;
76  background: var(--card);
77}
78
79.panel-head {
80  display: flex;
81  justify-content: space-between;
82  align-items: center;
83  gap: 10px;
84  margin-bottom: 14px;
85}
86
87.panel-actions {
88  display: flex;
89  gap: 6px;
90}
91
92.panel-actions button {
93  padding: 7px 12px;
94  border: 1px solid var(--border);
95  border-radius: 10px;
96  background: var(--card);
97  color: var(--text);
98  font-size: 0.85rem;
99  font-weight: 700;
100  cursor: pointer;
101}
102
103.states {
104  display: grid;
105  grid-template-columns: repeat(4, 1fr);
106  gap: 8px;
107  list-style: none;
108}
109
110.states li {
111  padding: 10px 12px;
112  border-radius: 12px;
113  background: #f5ede3;
114  font-size: 0.85rem;
115  font-weight: 700;
116}
117
118.states li small {
119  display: block;
120  margin-top: 2px;
121  color: var(--muted);
122  font-weight: 600;
123}
124
125.states li.on small {
126  color: var(--on);
127}
128
129.log {
130  margin-top: 14px;
131  padding: 12px 14px;
132  border-radius: 12px;
133  background: #2b1d14;
134  color: #f3e6d8;
135  font-family: ui-monospace, Consolas, monospace;
136  font-size: 0.8rem;
137  line-height: 1.8;
138  list-style: none;
139}
140
141.log li {
142  animation: fadeIn 0.3s;
143}
144
145@keyframes fadeIn {
146  from {
147    opacity: 0;
148  }
149}
150
151/* ---------- Buttons ---------- */
152.btn {
153  padding: 11px 16px;
154  border-radius: 12px;
155  font-size: 0.92rem;
156  font-weight: 800;
157  white-space: nowrap;
158  cursor: pointer;
159  transition: transform 0.2s, background 0.2s, box-shadow 0.2s;
160}
161
162.btn:active {
163  transform: scale(0.97);
164}
165
166.btn.solid {
167  border: 0;
168  background: var(--text);
169  color: #fff;
170}
171
172.btn.solid:hover {
173  box-shadow: 0 10px 24px -12px rgba(43, 29, 20, 0.8);
174}
175
176.btn.outline {
177  border: 1.5px solid var(--text);
178  background: none;
179  color: var(--text);
180}
181
182.btn.ghost {
183  border: 0;
184  background: none;
185  color: var(--text);
186  text-decoration: underline;
187  text-underline-offset: 3px;
188}
189
190.btn:focus-visible,
191.switch input:focus-visible + .slider,
192.cat-toggle:focus-visible,
193.close:focus-visible,
194.reopen:focus-visible {
195  outline: 3px solid rgba(194, 65, 12, 0.45);
196  outline-offset: 2px;
197}
198
199/* ---------- Banner ---------- */
200.banner {
201  position: fixed;
202  left: 24px;
203  bottom: 24px;
204  z-index: 40;
205  display: grid;
206  grid-template-columns: auto 1fr;
207  gap: 4px 16px;
208  width: min(480px, calc(100% - 32px));
209  padding: 22px;
210  border: 1px solid var(--border);
211  border-radius: 24px;
212  background: var(--card);
213  box-shadow: 0 30px 70px -30px rgba(43, 29, 20, 0.45);
214  animation: slideUp 0.6s var(--ease);
215}
216
217.banner.is-leaving {
218  animation: slideDown 0.35s var(--ease) forwards;
219}
220
221@keyframes slideUp {
222  from {
223    opacity: 0;
224    transform: translateY(40px);
225  }
226}
227
228@keyframes slideDown {
229  to {
230    opacity: 0;
231    transform: translateY(40px);
232  }
233}
234
235.cookie-art {
236  font-size: 2.6rem;
237  line-height: 1;
238  animation: wobble 2.4s ease-in-out infinite;
239}
240
241@keyframes wobble {
242  0%,
243  100% {
244    transform: rotate(0);
245  }
246  20% {
247    transform: rotate(-14deg);
248  }
249  40% {
250    transform: rotate(10deg);
251  }
252  60% {
253    transform: rotate(-4deg);
254  }
255}
256
257.banner h2 {
258  font-size: 1.15rem;
259  font-weight: 800;
260}
261
262.banner p {
263  margin-top: 6px;
264  color: var(--muted);
265  font-size: 0.9rem;
266  line-height: 1.55;
267}
268
269.gpc-note {
270  padding: 8px 10px;
271  border-radius: 10px;
272  background: #ecfdf3;
273  color: #166534 !important;
274}
275
276.banner-actions {
277  grid-column: 1 / -1;
278  display: flex;
279  justify-content: flex-end;
280  flex-wrap: wrap;
281  gap: 8px;
282  margin-top: 14px;
283}
284
285/* ---------- Preference center ---------- */
286.prefs {
287  width: min(600px, calc(100% - 24px));
288  max-height: min(760px, calc(100vh - 32px));
289  margin: auto;
290  padding: 0;
291  border: 0;
292  border-radius: 24px;
293  background: var(--card);
294  color: var(--text);
295  box-shadow: 0 40px 100px -30px rgba(43, 29, 20, 0.5);
296}
297
298.prefs[open] {
299  animation: pop 0.3s var(--ease);
300}
301
302@keyframes pop {
303  from {
304    opacity: 0;
305    transform: scale(0.96) translateY(10px);
306  }
307}
308
309.prefs::backdrop {
310  background: rgba(43, 29, 20, 0.4);
311  backdrop-filter: blur(3px);
312}
313
314.prefs-box {
315  display: flex;
316  flex-direction: column;
317  max-height: inherit;
318}
319
320.prefs-head {
321  display: flex;
322  justify-content: space-between;
323  align-items: center;
324  padding: 22px 24px 0;
325}
326
327.prefs-head h2 {
328  font-size: 1.35rem;
329  font-weight: 800;
330}
331
332.close {
333  display: grid;
334  place-items: center;
335  width: 34px;
336  height: 34px;
337  border: 0;
338  border-radius: 50%;
339  background: #f5ede3;
340  color: var(--muted);
341  cursor: pointer;
342}
343
344.prefs-intro {
345  padding: 8px 24px 16px;
346  color: var(--muted);
347  font-size: 0.92rem;
348  line-height: 1.55;
349}
350
351.cats {
352  display: grid;
353  gap: 10px;
354  padding: 0 24px 16px;
355  overflow-y: auto;
356}
357
358.cat {
359  border: 1px solid var(--border);
360  border-radius: 16px;
361  background: #fffaf4;
362}
363
364.cat-row {
365  display: flex;
366  align-items: center;
367  gap: 12px;
368  padding: 14px 16px;
369}
370
371.cat-toggle {
372  display: flex;
373  align-items: center;
374  gap: 10px;
375  flex: 1;
376  border: 0;
377  background: none;
378  color: var(--text);
379  text-align: left;
380  cursor: pointer;
381}
382
383.cat-toggle .chev {
384  color: var(--muted);
385  transition: transform 0.3s var(--ease);
386}
387
388.cat-toggle[aria-expanded="true"] .chev {
389  transform: rotate(90deg);
390}
391
392.cat-toggle b {
393  display: block;
394  font-size: 0.98rem;
395}
396
397.cat-toggle small {
398  color: var(--muted);
399  font-size: 0.82rem;
400}
401
402.always {
403  color: var(--on);
404  font-size: 0.8rem;
405  font-weight: 800;
406  white-space: nowrap;
407}
408
409/* Switch */
410.switch {
411  position: relative;
412  flex-shrink: 0;
413  cursor: pointer;
414}
415
416.switch input {
417  position: absolute;
418  opacity: 0;
419}
420
421.slider {
422  display: block;
423  width: 44px;
424  height: 24px;
425  border-radius: 999px;
426  background: #d6c8ba;
427  transition: background 0.25s;
428}
429
430.slider::after {
431  content: "";
432  position: absolute;
433  top: 3px;
434  left: 3px;
435  width: 18px;
436  height: 18px;
437  border-radius: 50%;
438  background: #fff;
439  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
440  transition: transform 0.25s var(--ease);
441}
442
443.switch input:checked + .slider {
444  background: var(--on);
445}
446
447.switch input:checked + .slider::after {
448  transform: translateX(20px);
449}
450
451/* Cookie details */
452.cat-details {
453  display: grid;
454  grid-template-rows: 0fr;
455  transition: grid-template-rows 0.35s var(--ease);
456}
457
458.cat-details > div {
459  overflow: hidden;
460}
461
462.cat.is-open .cat-details {
463  grid-template-rows: 1fr;
464}
465
466.cat-details p {
467  padding: 0 16px 10px;
468  color: var(--muted);
469  font-size: 0.86rem;
470  line-height: 1.55;
471}
472
473.cookies {
474  width: calc(100% - 32px);
475  margin: 0 16px 14px;
476  border-collapse: collapse;
477  font-size: 0.8rem;
478}
479
480.cookies th,
481.cookies td {
482  padding: 7px 8px;
483  border-bottom: 1px solid var(--border);
484  text-align: left;
485}
486
487.cookies th {
488  color: var(--muted);
489  font-weight: 700;
490}
491
492.cookies code {
493  font-family: ui-monospace, Consolas, monospace;
494  font-size: 0.78rem;
495}
496
497.prefs-foot {
498  display: flex;
499  justify-content: space-between;
500  align-items: center;
501  gap: 8px;
502  flex-wrap: wrap;
503  padding: 16px 24px 20px;
504  border-top: 1px solid var(--border);
505}
506
507.prefs-foot div {
508  display: flex;
509  gap: 8px;
510}
511
512/* ---------- Reopen button ---------- */
513.reopen {
514  position: fixed;
515  left: 20px;
516  bottom: 20px;
517  z-index: 30;
518  display: grid;
519  place-items: center;
520  width: 50px;
521  height: 50px;
522  border: 1px solid var(--border);
523  border-radius: 50%;
524  background: var(--card);
525  font-size: 1.5rem;
526  box-shadow: 0 14px 30px -14px rgba(43, 29, 20, 0.5);
527  cursor: pointer;
528  animation: slideUp 0.5s var(--ease);
529  transition: transform 0.25s var(--ease);
530}
531
532.reopen:hover {
533  transform: rotate(-20deg) scale(1.08);
534}
535
536/* ---------- Toast ---------- */
537.toast {
538  position: fixed;
539  left: 50%;
540  bottom: 24px;
541  z-index: 60;
542  padding: 11px 18px;
543  border-radius: 12px;
544  background: var(--text);
545  color: #fff;
546  font-size: 0.9rem;
547  font-weight: 700;
548  opacity: 0;
549  transform: translate(-50%, 16px);
550  transition: opacity 0.3s, transform 0.3s var(--ease);
551  pointer-events: none;
552}
553
554.toast.show {
555  opacity: 1;
556  transform: translate(-50%, 0);
557}
558
559/* ---------- Responsive ---------- */
560@media (max-width: 600px) {
561  .banner {
562    left: 12px;
563    right: 12px;
564    bottom: 12px;
565    width: auto;
566    grid-template-columns: 1fr;
567  }
568
569  .cookie-art {
570    display: none;
571  }
572
573  .banner-actions .btn {
574    flex: 1;
575  }
576
577  .states {
578    grid-template-columns: repeat(2, 1fr);
579  }
580
581  .prefs-foot,
582  .prefs-foot div {
583    flex-direction: column;
584    align-items: stretch;
585    width: 100%;
586  }
587
588  .cookies th:nth-child(3),
589  .cookies td:nth-child(3) {
590    display: none;
591  }
592}
593
594@media (prefers-reduced-motion: reduce) {
595  *,
596  *::before,
597  *::after {
598    animation: none !important;
599    transition: none !important;
600  }
601}

JavaScript

javascript
1// ---------- Your cookie categories (edit freely) ----------
2const CATEGORIES = [
3  {
4    id: "necessary",
5    name: "Strictly necessary",
6    short: "Login, security and your cookie choice",
7    locked: true,
8    text: "These keep the site working: signing in, keeping your cart, protecting forms and remembering this choice.",
9    cookies: [
10      ["session_id", "This site", "Keeps you signed in", "Session"],
11      ["csrf_token", "This site", "Protects forms from abuse", "Session"],
12      ["cookie-consent", "This site", "Remembers your choice", "180 days"],
13    ],
14  },
15  {
16    id: "functional",
17    name: "Functional",
18    short: "Live chat and saved preferences",
19    text: "Remember things like your language and power features such as live chat.",
20    cookies: [
21      ["lang", "This site", "Your language", "1 year"],
22      ["chat_uid", "Crisp", "Live chat history", "6 months"],
23    ],
24  },
25  {
26    id: "analytics",
27    name: "Analytics",
28    short: "Anonymous stats that help us improve",
29    text: "Tell us which pages are popular and where people get stuck. Data is aggregated and never sold.",
30    cookies: [
31      ["_ga", "Google Analytics", "Distinguishes visitors", "2 years"],
32      ["_ga_*", "Google Analytics", "Keeps session state", "2 years"],
33    ],
34  },
35  {
36    id: "marketing",
37    name: "Marketing",
38    short: "Relevant ads on other sites",
39    text: "Used by ad partners to show you offers you might like and to measure campaigns.",
40    cookies: [
41      ["_fbp", "Meta", "Ad delivery and measurement", "3 months"],
42      ["IDE", "Google Ads", "Ad personalisation", "13 months"],
43    ],
44  },
45];
46
47const STORAGE_KEY = "cookie-consent";
48const CONSENT_VERSION = 1; // bump when your categories change → everyone is asked again
49const MAX_AGE_DAYS = 180;
50
51// ---------- Storage ----------
52const $ = (id) => document.getElementById(id);
53const gpc = navigator.globalPrivacyControl === true;
54
55const read = () => {
56  try {
57    const saved = JSON.parse(localStorage.getItem(STORAGE_KEY));
58    const fresh = saved && Date.now() - saved.time < MAX_AGE_DAYS * 864e5;
59    return fresh && saved.version === CONSENT_VERSION ? saved : null;
60  } catch {
61    return null;
62  }
63};
64const write = (choices) => {
65  const record = { version: CONSENT_VERSION, time: Date.now(), choices };
66  try {
67    localStorage.setItem(STORAGE_KEY, JSON.stringify(record));
68  } catch {
69    /* storage blocked — the choice still applies for this visit */
70  }
71  return record;
72};
73
74const defaults = () => Object.fromEntries(CATEGORIES.map((c) => [c.id, Boolean(c.locked)]));
75let consent = read();
76
77// ---------- Demo log + live state panel ----------
78const logEl = $("log");
79window.consentLog = (msg) => {
80  if (logEl.dataset.empty !== "no") {
81    logEl.innerHTML = "";
82    logEl.dataset.empty = "no";
83  }
84  const time = new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
85  logEl.insertAdjacentHTML("beforeend", `<li>[${time}] ${msg}</li>`);
86};
87
88const paintStates = () => {
89  const choices = consent?.choices ?? defaults();
90  $("states").innerHTML = CATEGORIES.map(
91    (c) => `<li class="${choices[c.id] ? "on" : ""}">${c.name}<small>${choices[c.id] ? "● Allowed" : consent ? "○ Blocked" : "○ Not decided"}</small></li>`,
92  ).join("");
93};
94
95// ---------- Run scripts the visitor allowed ----------
96const runAllowedScripts = (choices) => {
97  document.querySelectorAll('script[type="text/plain"][data-consent]').forEach((tag) => {
98    if (!choices[tag.dataset.consent] || tag.dataset.ran) return;
99    tag.dataset.ran = "true";
100    const s = document.createElement("script");
101    s.textContent = tag.textContent;
102    document.head.appendChild(s);
103  });
104};
105
106// ---------- Apply a decision ----------
107const toast = (() => {
108  let t;
109  return (msg) => {
110    $("toast").textContent = msg;
111    $("toast").classList.add("show");
112    clearTimeout(t);
113    t = setTimeout(() => $("toast").classList.remove("show"), 2400);
114  };
115})();
116
117const apply = (choices, message) => {
118  const before = consent?.choices ?? {};
119  consent = write({ ...choices, necessary: true });
120  runAllowedScripts(consent.choices);
121  CATEGORIES.forEach((c) => {
122    if (before[c.id] && !consent.choices[c.id]) consentLog(`⛔ ${c.name} turned off — takes full effect after a reload`);
123  });
124  hideBanner();
125  $("reopen").hidden = false;
126  if ($("prefs").open) $("prefs").close();
127  paintStates();
128  toast(message);
129  // Let the rest of your site react
130  window.dispatchEvent(new CustomEvent("cookieconsent:change", { detail: { ...consent.choices } }));
131};
132
133const acceptAll = () => apply(Object.fromEntries(CATEGORIES.map((c) => [c.id, true])), "All cookies allowed 🍪");
134const rejectAll = () => apply(defaults(), "Only necessary cookies — got it");
135
136// ---------- Banner ----------
137const banner = $("banner");
138const showBanner = () => {
139  banner.hidden = false;
140  banner.classList.remove("is-leaving");
141  $("gpc-note").hidden = !gpc;
142};
143const hideBanner = () => {
144  if (banner.hidden) return;
145  banner.classList.add("is-leaving");
146  setTimeout(() => (banner.hidden = true), 350);
147};
148
149// ---------- Preference center ----------
150const prefs = $("prefs");
151const renderCats = () => {
152  const choices = consent?.choices ?? { ...defaults(), marketing: false };
153  $("cats").innerHTML = CATEGORIES.map(
154    (c) => `<div class="cat">
155      <div class="cat-row">
156        <button type="button" class="cat-toggle" aria-expanded="false" aria-controls="details-${c.id}">
157          <svg class="chev" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>
158          <span><b>${c.name}</b><small>${c.short}</small></span>
159        </button>
160        ${
161          c.locked
162            ? '<span class="always">Always on</span>'
163            : `<label class="switch"><input type="checkbox" data-cat="${c.id}" ${choices[c.id] ? "checked" : ""} aria-label="${c.name} cookies" /><span class="slider"></span></label>`
164        }
165      </div>
166      <div class="cat-details" id="details-${c.id}"><div>
167        <p>${c.text}</p>
168        <table class="cookies"><thead><tr><th>Cookie</th><th>Provider</th><th>Purpose</th><th>Expires</th></tr></thead>
169        <tbody>${c.cookies.map(([n, p, u, e]) => `<tr><td><code>${n}</code></td><td>${p}</td><td>${u}</td><td>${e}</td></tr>`).join("")}</tbody></table>
170      </div></div>
171    </div>`,
172  ).join("");
173};
174
175const openPrefs = () => {
176  renderCats();
177  prefs.showModal();
178};
179
180$("cats").addEventListener("click", (e) => {
181  const t = e.target.closest(".cat-toggle");
182  if (!t) return;
183  const open = t.getAttribute("aria-expanded") !== "true";
184  t.setAttribute("aria-expanded", open);
185  t.closest(".cat").classList.toggle("is-open", open);
186});
187
188$("save").addEventListener("click", () => {
189  const choices = defaults();
190  $("cats").querySelectorAll("[data-cat]").forEach((i) => (choices[i.dataset.cat] = i.checked));
191  apply(choices, "Your preferences are saved");
192});
193
194$("prefs-close").addEventListener("click", () => prefs.close());
195prefs.addEventListener("click", (e) => e.target === prefs && prefs.close());
196
197document.querySelectorAll("[data-accept]").forEach((b) => b.addEventListener("click", acceptAll));
198document.querySelectorAll("[data-reject]").forEach((b) => b.addEventListener("click", rejectAll));
199document.querySelectorAll("[data-open-prefs]").forEach((b) => b.addEventListener("click", openPrefs));
200$("reopen").addEventListener("click", openPrefs);
201
202// Demo: forget the choice and start over
203$("reset").addEventListener("click", () => {
204  try {
205    localStorage.removeItem(STORAGE_KEY);
206  } catch {
207    /* storage blocked */
208  }
209  consent = null;
210  $("reopen").hidden = true;
211  logEl.dataset.empty = "";
212  logEl.innerHTML = "<li>Consent cleared — waiting for a choice…</li>";
213  paintStates();
214  showBanner();
215});
216
217// ---------- Small public API for the rest of your site ----------
218window.cookieConsent = {
219  get: () => ({ ...(consent?.choices ?? defaults()) }),
220  has: (category) => Boolean(consent?.choices?.[category]),
221  open: openPrefs,
222  reset: () => $("reset").click(),
223};
224
225// ---------- Start ----------
226paintStates();
227if (consent) {
228  $("reopen").hidden = false;
229  runAllowedScripts(consent.choices);
230} else {
231  setTimeout(showBanner, 700);
232}

Built by MetaSkillGroup — copy it, use it, make it yours.

Features

  • Banner with Accept all, Reject all and Customize

  • Preference center with switches and expandable cookie tables

  • Blocks <script type="text/plain" data-consent> until allowed

  • Respects Global Privacy Control (marketing off by default)

  • Consent saved for 180 days with a version number

  • Floating 🍪 button to reopen preferences

  • window.cookieConsent API + cookieconsent:change event

Built With

🌐

HTML5

🎨

CSS3

⚡

JavaScript

Compatibility

Browsers
ChromeEdgeFirefoxSafariOperaBraveArcVivaldi
Devices
DesktopTabletMobile
Frameworks
Vanilla JSHTML/CSS

❓ FAQs

Change your script tag to type="text/plain" and add data-consent="analytics" (or functional / marketing). It runs automatically once that category is allowed.

Use window.cookieConsent.has('analytics'), or listen for the cookieconsent:change event on window — its detail holds every category's choice.

Increase CONSENT_VERSION in script.js. Saved choices with an older version are ignored, so the banner shows again.

MetaSkillGroup

Written by

MetaSkillGroup

October 15, 2026

Need it built for you?

Built to the same standard as our client work.

If nothing here fits exactly, tell us what you need — our core developers build it. Requirements call first, a written SRS before any code, and a price agreed up front.

Cookie Consent Banner with Preferences (HTML JS) | MetaSkillGroup