- No data uploaded
- Instant results
Tailwind CSS Cookie Consent Banner for Vanilla JS
A production-ready minimal cookie consent banner: a GDPR-style cookie banner with equally prominent “Accept all” and “Reject all”, granular analytics/marketing toggles (off by default), stored consent and Google Consent Mode v2 signals. Pick your brand color, toggle dark mode and corner radius — the live preview and the Vanilla JS code update instantly in your browser.
Live component editor
<!-- Requires Tailwind CSS v3 (CLI/PostCSS build, or the Play CDN for prototypes) -->
<div>
<div data-show="banner">
<div class="fixed inset-x-0 bottom-0 z-50 p-4" role="region" aria-label="Cookie consent">
<div class="max-w-4xl mx-auto p-6 bg-white border border-slate-200 shadow-sm rounded-xl">
<h2 class="text-lg font-semibold text-slate-900">We value your privacy</h2>
<p class="mt-2 text-sm text-slate-500">We use essential cookies to run this site. With your permission we also use analytics and marketing cookies. You can change your choice at any time. <a href="#" class="font-medium text-[#4f46e5] hover:underline">Cookie policy</a></p>
<div data-show="prefs" hidden>
<div class="mt-4 flex flex-col gap-3">
<label class="flex items-start gap-3 text-sm text-slate-700">
<input type="checkbox" class="h-4 w-4 accent-[#4f46e5] mt-1" checked disabled>
<span><strong>Strictly necessary</strong> — security, load balancing, your consent choice. Always on.</span>
</label>
<label class="flex items-start gap-3 text-sm text-slate-700">
<input id="cc-analytics" type="checkbox" class="h-4 w-4 accent-[#4f46e5] mt-1">
<span><strong>Analytics</strong> — anonymous usage statistics that help us improve.</span>
</label>
<label class="flex items-start gap-3 text-sm text-slate-700">
<input id="cc-marketing" type="checkbox" class="h-4 w-4 accent-[#4f46e5] mt-1">
<span><strong>Marketing</strong> — personalised ads and campaign measurement.</span>
</label>
</div>
</div>
<div class="mt-5 flex flex-wrap items-center gap-2">
<button type="button" class="inline-flex items-center justify-center px-4 py-2.5 text-sm font-semibold rounded-lg bg-[#4f46e5] hover:bg-[#433cc3] text-white shadow-sm transition-colors focus:outline-none focus:ring-2 focus:ring-[#4f46e5] focus:ring-offset-2 focus:ring-offset-white" data-call="rejectAll" data-close="banner">Reject all</button>
<button type="button" class="inline-flex items-center justify-center px-4 py-2.5 text-sm font-semibold rounded-lg bg-[#4f46e5] hover:bg-[#433cc3] text-white shadow-sm transition-colors focus:outline-none focus:ring-2 focus:ring-[#4f46e5] focus:ring-offset-2 focus:ring-offset-white" data-call="acceptAll" data-close="banner">Accept all</button>
<button type="button" class="inline-flex items-center justify-center px-3 py-2 text-sm font-semibold rounded-lg transition-colors text-slate-700 hover:bg-slate-900/5" data-toggle="prefs" data-expanded="prefs" aria-expanded="false">Customize</button>
<div data-show="prefs" hidden>
<button type="button" class="inline-flex items-center justify-center px-4 py-2.5 text-sm font-semibold rounded-lg border transition-colors border-slate-300 text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-[#4f46e5] focus:ring-offset-2 focus:ring-offset-white" data-call="savePreferences" data-close="banner">Save preferences</button>
</div>
</div>
</div>
</div>
</div>
<div data-hide="banner" hidden>
<button type="button" class="fixed bottom-0 left-0 m-4 inline-flex items-center justify-center px-4 py-2.5 text-sm font-semibold rounded-lg border transition-colors border-slate-300 text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-[#4f46e5] focus:ring-offset-2 focus:ring-offset-white" data-open="banner">Cookie settings</button>
</div>
</div>
<script>
(() => {
const CONSENT_KEY = 'cookie-consent-v1'; // bump the version to re-ask after policy changes
function readConsent() {
try {
return JSON.parse(localStorage.getItem(CONSENT_KEY));
} catch {
return null; // storage blocked / SSR → treat as "not decided"
}
}
function saveConsent(choice) {
const record = { necessary: true, ...choice, timestamp: new Date().toISOString() };
try {
localStorage.setItem(CONSENT_KEY, JSON.stringify(record));
} catch {}
// Google Consent Mode v2: only grant what the user chose.
window.gtag?.('consent', 'update', {
analytics_storage: choice.analytics ? 'granted' : 'denied',
ad_storage: choice.marketing ? 'granted' : 'denied',
ad_user_data: choice.marketing ? 'granted' : 'denied',
ad_personalization: choice.marketing ? 'granted' : 'denied'
});
// Load optional scripts by listening for this event.
document.dispatchEvent(new CustomEvent('cookie-consent', { detail: record }));
}
function acceptAll() {
saveConsent({ analytics: true, marketing: true });
}
function rejectAll() {
saveConsent({ analytics: false, marketing: false });
}
function savePreferences() {
saveConsent({
analytics: Boolean(document.getElementById('cc-analytics')?.checked),
marketing: Boolean(document.getElementById('cc-marketing')?.checked)
});
}
const state = { banner: !readConsent(), prefs: false };
const actions = { acceptAll, rejectAll, savePreferences };
const pair = (s) => s.split(':');
const sync = () => {
document.querySelectorAll('[data-show]').forEach((el) => (el.hidden = !state[el.dataset.show]));
document.querySelectorAll('[data-hide]').forEach((el) => (el.hidden = !!state[el.dataset.hide]));
document.querySelectorAll('[data-when]').forEach((el) => { const [k, v] = pair(el.dataset.when); el.hidden = state[k] !== v; });
document.querySelectorAll('[data-pressed]').forEach((el) => { const [k, v] = pair(el.dataset.pressed); el.setAttribute('aria-pressed', String(state[k] === v)); });
document.querySelectorAll('[data-expanded]').forEach((el) => el.setAttribute('aria-expanded', String(!!state[el.dataset.expanded])));
};
document.addEventListener('click', (e) => {
const t = e.target.closest('[data-toggle],[data-open],[data-close],[data-set],[data-call]');
if (!t) return;
const { toggle, open, close, set, call } = t.dataset;
if (call) actions[call]?.(e);
if (toggle) state[toggle] = !state[toggle];
if (open) state[open] = true;
if (close) state[close] = false;
if (set) { const [k, v] = pair(set); state[k] = v; }
sync();
});
sync();
})();
</script>
More Tailwind components for Vanilla JS
All components →- Footer A multi-column site footer with a brand description, three link groups, a newsletter sign-up field and a copyright row
- Login Modal A centered sign-in dialog with email and password fields, “remember me”, a forgot-password link and a social sign-in button
- Signup Form A registration card with name, email and password fields, a password hint, a terms-of-service checkbox and a primary call to action
- Navbar A responsive top navigation bar with a logo, primary links, a sign-in/CTA pair on desktop and a collapsible hamburger menu on mobile
- Contact Form A contact card with name, email, a topic selector, a message textarea and a submit button
- Pricing Table A three-tier pricing section with a monthly/yearly billing toggle, a highlighted “most popular” plan and feature checklists
- Hero Section A landing-page hero with an announcement badge, a large headline, supporting copy, primary and secondary calls to action and a row of social-proof stats
- Feature Grid A marketing section with a centered heading and a responsive grid of six feature cards, each with an icon badge, title and description
Install Tailwind CSS for Vanilla JS
Prototype instantly with the Play CDN, or install the Tailwind CLI for production:
<script src="https://cdn.tailwindcss.com"></script>npm install -D tailwindcss@3
npx tailwindcss init
npx tailwindcss -i ./src/input.css -o ./dist/output.css --watchAdd ./**/*.html to content in tailwind.config.js and put the three @tailwind directives in src/input.css.
Use the cookie consent banner in Vanilla JS
Paste the markup where the component should appear. The small inline <script> wires up the interactive state with one delegated click listener — no framework needed.
Making the banner actually compliant
- Block before consent: load analytics/marketing tags only after the
cookie-consentevent reports the matching category (or set Google Consent Mode v2 defaults todeniedbefore tags load). - Equal choice: keep “Reject all” on the first layer with the same visual weight as “Accept all”.
- Withdrawal: keep the “Cookie settings” button (or a footer link) available on every page.
- Record: the stored choice includes a timestamp; bump
CONSENT_KEYwhen your purposes change to re-ask.
This is an implementation pattern, not legal advice — have your privacy counsel review the wording and categories for your jurisdictions.
Accessibility & customization
The banner is a labelled region (role="region" with aria-label) rather than a blocking modal, preference toggles are native checkboxes with labels, and the always-available “Cookie settings” button lets keyboard users reopen it.
Every color, radius and dark-mode choice is baked into plain utility classes, so the copied code has no runtime dependency on this site.
Compatibility
| Item | Details |
|---|---|
| CSS framework | Tailwind CSS v3.4+ |
| Library | Plain HTML + optional vanilla JS |
| Interactive state | banner, prefs |
| Dark mode | Yes — toggle in the editor |
| Style | Minimal |