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
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
- Google Fonts — Karla + Fraunces https://fonts.googleapis.com/css2?family=Karla:wght@400;500;600;700;800&family=Fraunces:ital,opsz,wght@1,9..144,600&display=swap
Already linked inside the component's files — no extra setup needed.
Desktop Preview
HTML
html1<!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
css1/* 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
javascript1// ---------- 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
❓ 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.

Written by
MetaSkillGroup
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.