Cookie banner
A cookie consent banner, a small card of ink in a corner of the window, done properly.
Documentation
-
Copy to Webflow
Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: Webflow removes any font your site has not installed. -
Add custom CSS
Custom CSS in WebflowThis CSS already ships inside the Webflow embed, so a pasted section needs nothing added. Paste it yourself, in the page or the site custom code, only if the effect runs on several pages. The tutorial explains where.
See the tutorialThe look of the banner is thecookie-banner-*classes of the paste; the Embed holds what fixes it to the window, hides it and moves it. Paste it as a direct child of the page, with nothing above it that carries a transform or a filter, make it a Component and place it on every page. The block named "Page of the preview" is the demo around the banner: delete it on your site, and keep the banner and the Embed. The buttons and the switches are Link blocks, because Webflow has no button that holds layers; the script gives each the role and the keys of a button. -
Add custom JS
Custom JavaScript in WebflowThis script already ships inside the Webflow embed, settings block included. You paste it yourself, before the closing body tag of the page or the site custom code, only when the effect has to run on several pages, and the tutorial says how to choose.
See the tutorial -
Use it on your own elements
How to add a custom attribute in WebflowA custom attribute is a pair, a name and a value, typed in the Settings panel of the element you selected, under Custom attributes. The name is what the script looks for, the value is what it reads. The tutorial shows the panel.
See the tutorialCustom attributesSelect the element on the canvas and open the Settings panel. Under Custom attributes, add a pair: the name on the left, the value on the right.Custom attribute What it does data-cookie-banner Marks the banner: the block that holds the words, the answers and the categories. It takes no value: leave the value field empty. The settings below go on the same element. data-cookie-banner-refuse On the button that refuses everything but the locked categories. It takes no value. Give it the same class as the one that accepts: the two answers must weigh the same. [ Required ] data-cookie-banner-accept On the button that accepts every category. It takes no value. [ Required ] data-cookie-banner-choose On what opens and folds back the categories. It takes no value. The engine writes aria-expanded on it. Without it, and without a panel, the banner has two answers and nothing else. [ Optional ] data-cookie-banner-panel On the box Choose opens: it holds one box, which holds the categories and the save button. It takes no value. The stylesheet folds it to no height until it is asked for. [ Optional ] data-cookie-banner-category="measurement" On the box of one category; the value is its name, yours to choose, and the name gated scripts refer to. Add or remove boxes freely: the engine reads the list. A category the stored choice has never seen asks everyone again. [ Optional ] data-cookie-banner-switch On the switch of a category, inside its box. It takes no value. The engine gives it role="switch" and aria-checked; the whole box answers a click. [ Optional ] data-cookie-banner-title On the name of a category: it names the switch for a screen reader. It takes no value. [ Optional ] data-cookie-banner-locked On a category that cannot be refused, the necessary one: its switch is on and stays there. It takes no value. [ Optional ] data-cookie-banner-google="analytics_storage" On a category: the Google Consent Mode signals it answers for, separated by spaces. Read only when gtag is on the page. [ Optional ] data-cookie-banner-save On the button that stores the switches as they are. It takes no value. [ Optional ] data-cookie-banner-knob On the knob inside a switch, so the stylesheet can move it. It takes no value. [ Optional ] data-cookie-banner-run="measurement" On a script of the page written type="text/plain": it only runs once the category named is allowed. Also on an iframe or an image that carries data-src in place of src. [ Optional ] data-cookie-banner-open On anything of the page, a footer link for one: a press opens the banner on its categories. It takes no value. Needed when the setting mark is none. [ Optional ] data-cookie-banner-state Written by the engine on the banner: open, wide once it is drawn out, choosing while the categories show, blank while the words are away. Never written by hand; the stylesheet reads it, and so can a rule of yours. [ Optional ] SettingsEvery setting is a custom attribute too, added the same way and on the same element. An attribute always wins over the defaults written in the code.Attribute Default What it is data-cookie-banner-accent #ffab1a The colour of the edge that draws the banner out and takes it back, of the line on the mark, of a switch that is on and of the plate that rises on a button. Options: any CSS colour. The other colours are the banner's own classes: the engine reads the ink from the background of the banner. data-cookie-banner-mark left The corner the banner goes back to, where the mark stays. Options: left, the bottom left corner, the banner held to the left; right, the bottom right; none, no mark at all, the banner goes to the left and is gone, and something of the page marked data-cookie-banner-open must open it again. data-cookie-banner-key cookie-banner The name the choice is stored under in localStorage. Options: any text. Two sites on one domain that must not share a choice take two names; two banners with one name share theirs. data-cookie-banner-version 1 The version of what is asked. A stored choice of another version is no choice. Options: any text. Change it when the categories or what they cover change, and everyone is asked again. data-cookie-banner-days 180 How long a choice is kept before the banner asks again, in days. data-cookie-banner-delay 600 The wait before the banner first comes in, in milliseconds, for someone who has not chosen. data-cookie-banner-travel 1.3 The time the edge takes to draw the banner out of its mark, or to take it back, in seconds: up then across, or across then down. data-cookie-banner-reload 1 What happens when a category whose scripts already ran is withdrawn. Options: 1 reloads the page, the only way to stop a script that runs; 0 leaves the page as it is, for a site that stops its own tags on cookiebanner:change. data-cookie-banner-label Cookie settings The name of the mark, read by a screen reader and written beside it when the edge moves out under the pointer; also the name of the banner when its markup gives none. Options: any text, in the language of the page. data-cookie-banner-shown Cookie choices are open. The sentence a screen reader is told when the banner comes in. Options: any text. data-cookie-banner-saved Your cookie choice is saved. The sentence a screen reader is told once a choice is stored. Options: any text. Gating your scriptsThe banner only holds back what you mark. In Site settings, Custom code, or in an Embed, write each tag that needs consent withtype="text/plain"anddata-cookie-banner-runset to the name of its category: the browser reads it as text until that category is allowed, then the script writes it again as a real one. A tag left as a normal script runs whatever the visitor answers. The two test scripts at the end of the pasted Embed are the preview's: replace them with yours, or delete them. It is the mechanism, not legal advice: the words, the categories and the policy link are yours to write.Custom attribute What it does type="text/plain" On the script: what keeps the browser from running it. Not a custom attribute: it is written in the tag itself. A module is written with data-type="module" beside it. data-cookie-banner-run="measurement" On the same script: the category that lets it through, one of the names of the banner's categories. A name the banner does not list is let through by Accept all only. data-src="https://..." On an iframe or an image marked data-cookie-banner-run, in place of src: the address it only loads once its category is allowed.
Two files, no build step.
It runs in the browser only. Load it with a script tag in plain HTML, in a
client-side script in Astro, never in the frontmatter. In React, call
CookieBanner.mount(ref.current) in useEffect and
destroy() in its cleanup.
-
Tune it, either way
Per element with an attribute, or once for the whole site by editingDEFAULTSat the top of the file.Attribute Default What it is Lower Higher data-cookie-banner-accent #ffab1a The colour of the edge that draws the banner out and takes it back, of the line on the mark, of a switch that is on and of the plate that rises on a button. Options: any CSS colour. The other colours are the banner's own classes: the engine reads the ink from the background of the banner. data-cookie-banner-mark left The corner the banner goes back to, where the mark stays. Options: left, the bottom left corner, the banner held to the left; right, the bottom right; none, no mark at all, the banner goes to the left and is gone, and something of the page marked data-cookie-banner-open must open it again. data-cookie-banner-key cookie-banner The name the choice is stored under in localStorage. Options: any text. Two sites on one domain that must not share a choice take two names; two banners with one name share theirs. data-cookie-banner-version 1 The version of what is asked. A stored choice of another version is no choice. Options: any text. Change it when the categories or what they cover change, and everyone is asked again. data-cookie-banner-days 180 How long a choice is kept before the banner asks again, in days. Asks again sooner Asks less often; many authorities advise about six months, which is the default data-cookie-banner-delay 600 The wait before the banner first comes in, in milliseconds, for someone who has not chosen. Comes in sooner; 0 comes in at once Leaves the page time to settle first data-cookie-banner-travel 1.3 The time the edge takes to draw the banner out of its mark, or to take it back, in seconds: up then across, or across then down. Brisker; under 0.8 the two moves read as one Slower and more ceremonious; past 2 it holds the eye too long data-cookie-banner-reload 1 What happens when a category whose scripts already ran is withdrawn. Options: 1 reloads the page, the only way to stop a script that runs; 0 leaves the page as it is, for a site that stops its own tags on cookiebanner:change. data-cookie-banner-label Cookie settings The name of the mark, read by a screen reader and written beside it when the edge moves out under the pointer; also the name of the banner when its markup gives none. Options: any text, in the language of the page. data-cookie-banner-shown Cookie choices are open. The sentence a screen reader is told when the banner comes in. Options: any text. data-cookie-banner-saved Your cookie choice is saved. The sentence a screen reader is told once a choice is stored. Options: any text. Gating your scriptsThe banner only holds back what you mark. Write each script that needs consent withtype="text/plain"anddata-cookie-banner-runset to the name of its category: the browser reads it as text until that category is allowed, at the choice or at the next page load, and the engine then writes it again as a real script, in place, once. A script left as it was runs whatever the visitor answers. It is the mechanism, not legal advice: the words, the categories and the policy link are yours to write.Custom attribute What it does type="text/plain" On the script: what keeps the browser from running it. Not a custom attribute: it is written in the tag itself. A module is written with data-type="module" beside it. data-cookie-banner-run="measurement" On the same script: the category that lets it through, one of the names of the banner's categories. A name the banner does not list is let through by Accept all only. data-src="https://..." On an iframe or an image marked data-cookie-banner-run, in place of src: the address it only loads once its category is allowed. -
Drive it from JavaScript
When the site's own code needs the choice: to load something by hand, to tell a server, or to open the banner from a link.const element = document.querySelector('[data-cookie-banner]'); element.addEventListener('cookiebanner:change', (e) => { console.log(e.detail); }); // Read the choice anywhere, even before the banner is mounted. console.log(CookieBanner.get(), CookieBanner.allowed('measurement')); // Be told at each choice. document.addEventListener('cookiebanner:change', (event) => { const { choice, accepted, refused, withdrawn } = event.detail; // event.preventDefault() here keeps the page from reloading on a withdrawal }); const banner = CookieBanner.mount(element); // the banner auto() mounted, or a new one banner.open(); // draws it out; CookieBanner.open() does it from anywhere, focus included // banner.reset() forgets the choice and asks again // When the banner leaves the page: CookieBanner.destroy(element); // removes what the engine added, gives the markup back as it was
-
Fit it to your page
What a buyer changes
The words, the policy link and the categories are plain markup: write yours, add or remove a category box, and the engine follows. Then mark each script that needs consent with
type="text/plain"anddata-cookie-banner-run="category".The markup
data-cookie-banneron the banner. It takes no value. The engine gives itrole="region"and a name when the markup has none.data-cookie-banner-refuse,data-cookie-banner-accepton the two answers. Give them one class: the same weight is the point.data-cookie-banner-chooseon what opens the categories, anddata-cookie-banner-panelon what it opens: a box holding one box, which holds the categories.data-cookie-banner-saveon the button that stores the switches as they are. A banner with no panel is a banner with two answers, and works.data-cookie-banner-category="name"on the box of one category, with inside it an element markeddata-cookie-banner-switch, anddata-cookie-banner-titleon its name, which names the switch for a screen reader. The names are yours;measurementandmarketingare the preview's. The whole box answers a click.data-cookie-banner-lockedon a category that cannot be refused: its switch is on and stays there.data-cookie-banner-google="analytics_storage"on a category names the Google Consent Mode signals it answers for, separated by spaces.data-cookie-banner-openon anything of the page, a footer link for one: a press opens the banner on its categories.
Gating a script
html <script type="text/plain" data-cookie-banner-run="measurement" src="https://example.com/tag.js"></script> <script type="text/plain" data-cookie-banner-run="marketing"> /* inline code, run once marketing is allowed */ </script> <iframe data-cookie-banner-run="marketing" data-src="https://www.youtube-nocookie.com/embed/..."></iframe>A browser does not run
text/plain. When the category is allowed, at the choice or at the next page load, the engine writes the script again as a real one, in the same place, once, external scripts in the order of the page. A module is writtendata-type="module". Any other element withdata-cookie-banner-runand adata-srcgets itssrcthe same way. A category the banner does not list is allowed by Accept all only. CallCookieBanner.scan()after adding gated scripts to the page by code.Google Consent Mode
When
gtagis a function of the page, the engine callsgtag('consent', 'update', {...})withgrantedordeniedfor every signal the categories name, at each choice and at each load that finds one. The default is yours to set, before the Google tag:html <script> window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag('consent', 'default', { ad_storage: 'denied', ad_user_data: 'denied', ad_personalization: 'denied', analytics_storage: 'denied' }); </script>The colours and the places
The look of the banner is its classes. The engine draws the edge, the mark and the states with two colours:
accent, a setting, and the ink, which it reads from the background of the banner. Three lengths are custom properties to set in a rule of your own on[data-cookie-banner], [data-cookie-banner-tab]:--cookie-banner-gap, how far the banner and the mark keep from the edges,--cookie-banner-size, the side of the mark,--cookie-banner-z, their layer.From JavaScript
CookieBanner.get()returns the choice,{ necessary: true, measurement: false, ... }, ornullwhen there is none; it can be called before the banner is mounted.CookieBanner.allowed('measurement')answers for one category.CookieBanner.open()opens the banner on its categories,CookieBanner.reset()forgets the choice and asks again,CookieBanner.scan()looks for gated scripts again.CookieBanner.mount(element, overrides)returns the instance of a banner, mounting it if it was not:{ element, settings, choice(), allowed(category), open(choosing), close(), choose(shown), toggle(category, on), accept(), refuse(), save(), set(map), reset(), scan(), destroy() }.close()puts the banner away without storing anything.CookieBanner.auto(root)mounts every marked banner, and runs once on its own;CookieBanner.destroy(element)gives the markup back.- Events, all bubbling from the banner:
cookiebanner:ready,cookiebanner:open,cookiebanner:close,cookiebanner:runfor each gated script let through, andcookiebanner:changeat each choice, with{ choice, accepted, refused, withdrawn };preventDefault()on it keeps the page from reloading when something was withdrawn. - The state is on the banner as
data-cookie-banner-state:open,wideonce it is drawn out,choosingwhile the categories show,blankwhile the words are away. A switch carriesaria-checked, the markdata-cookie-banner-tab="shown". - Keyboard. Every control is a button: Tab reaches it, Enter and Space press it. Escape folds the categories back and returns to Choose. The banner is not a dialog and holds no focus: Tab goes on into the page. Opened from the mark, the banner takes the focus; once the choice is made the focus goes to the mark. While the banner is open the page scrolls what has the focus clear of it.
- On a touch screen nothing changes but the layout: under 768px the banner is a sheet, the two answers side by side, Choose under them, the categories one above the other, the whole line of each a target.
- **Under
prefers-reduced-motion: reduce** the banner and the mark are simply there or not: no travel, no transition. - Without JavaScript the banner stays hidden and no gated script runs.
- Several banners on a page each keep their own state; they share a choice when they share a
key. A gated script runs as soon as one of them allows its category. - The words of the demo are the demo's: on a real site every sentence is the site's own, in its language, or asked for.
-
Avoid the pitfalls
- A script that is not marked is not gated. The banner only holds back what is written
type="text/plain"withdata-cookie-banner-run. A tag manager loaded as a normal script loads whatever the answer. - Put the banner on every page, with the same
key: in Webflow, make the pasted block a Component and place it on each page, or in a footer that is one. The code may then live once in the site's custom code. - Keep it out of a box that moves. The banner and the mark are
position: fixed: inside an element with atransform, afilteror aperspectivethey are fixed to that element instead of the window. Paste it as a direct child of the page. - Early in the page is better. The banner comes where it is in the markup under Tab: placed first in the body, a keyboard reaches it first.
- **Change
versionwhen the categories change.** A category added to the markup asks again by itself; one whose meaning changed does not. - Withdrawing reloads. With
reloadat1a visitor who switches off a category that already ran sees the page load again. Set it to0only if the site stops its tags itself. - localStorage is per origin.
www.example.comandshop.example.comeach ask. Where the browser refuses storage, private windows of some browsers, the choice lasts as long as the page. - The server cannot read it. The choice is in localStorage, not in a cookie: a server that must know reads
cookiebanner:changeand is told. - In Webflow the buttons and the switches are Link blocks, since Webflow has no button that holds layers; the engine gives each the role and the keys of a button. The Designer canvas runs no code, so there the banner sits in the flow, categories open, every word editable; it is fixed to the window in preview and on the published site.
- **
destroy()cannot take back a script that ran.** It removes the mark, the edge, the listeners and every attribute the engine wrote.
- A script that is not marked is not gated. The banner only holds back what is written
One click on Copy the AI prompt copies everything an assistant needs to build this effect: the complete code, the markup, the dependency, every setting and every pitfall.
What it does
Cookie banner is a site utility component for Webflow and vanilla JavaScript, with no library. Also called cookie consent banner, cookie consent or gdpr cookie banner.
A cookie consent banner, a small card of ink in a corner of the window, done properly. It offers Accept all and Refuse all as two buttons of the same class, the same size and the same hover, and Choose, which opens the categories: the necessary one, always on, then the ones the site declares in plain markup, each with a real switch, coming up one after the other. It moves in three beats. A small square stands in the corner; an edge of the accent colour draws the card out of it, up then across, the content sitting still while the edge uncovers it; the words come up. Once the choice is made the words leave, the edge sweeps back and the card is the square again: the mark, with the edge on its side. Under the pointer or the keyboard the edge moves out and uncovers the name of the mark; pressed, it draws the banner out again, the switches as they were left. The choice is stored in localStorage with a version and a duration, so nobody who chose is asked again until one of them changes. And it is a real gate: a script of the page written type="text/plain" with the name of a category only runs once that category is allowed, an event is fired with the choice, a small API reads it, Google Consent Mode is updated when gtag is on the page, and withdrawing a category reloads the page so what ran stops. It is not a dialog: the page stays in reach, the focus is never trapped, a live region says what happened, reduced motion gets the banner and the mark with no travel, the phone gets a sheet with two equal answers side by side, and a page without JavaScript shows no banner and runs no gated script. No library. It is the mechanism, not legal advice: the words, the categories and the policy link are the site's own. Pattern type cookie banner, cookie consent, consent manager.
Updated
Questions
Paste the banner: a block marked data-cookie-banner with its words, two answers, three categories and one Embed that holds all the code. Make it a Component, place it on every page, write your own text and policy link, and the choice is stored in the visitor's browser, with no account and no outside script.
Write each tag that needs consent with type="text/plain" and data-cookie-banner-run="measurement", the value being the name of one of your categories. The browser reads it as text, and the engine writes it again as a real script once that category is allowed, at the choice or at the next page load. A script that is not marked is not held back.
Yes, when gtag is on the page. Each category names its signals in data-cookie-banner-google, analytics_storage for one, and the engine sends gtag('consent', 'update', ...) with granted or denied at each choice and at each load that finds one. The default, denied, is yours to set before the Google tag.
No banner is, alone. This one is the mechanism: nothing gated runs before a choice, Refuse all is as easy as Accept all, no switch is on in advance, and the choice can be changed from the mark in the corner. The text, the list of categories, the policy page and marking every script are yours, and it is not legal advice.
The choice is kept in localStorage for the number of days of the setting days, 180 by default. Change data-cookie-banner-version and every visitor is asked again; adding a category to the markup does it too. CookieBanner.reset() forgets the choice of the current visitor.