The smallest legally compliant cookie consent banner in existence.
~7KB minified + gzipped. Zero dependencies. TypeScript. Works with React, Vue, Angular, Svelte, or vanilla JS.
Read more about the library and see it in action on my blog
If you use this library and want a mention here, send me your URL!
- ~7KB gzipped — still smaller than most images
- Zero dependencies — no bloat, no supply chain risk
- No external requests — works offline, no tracking
- 100% customizable — every string, every style, every behavior
- Full i18n — localize to any language (EN, NL, DE, ES, ZH, JA, etc.)
- CSS variables — style with your own design system
- Framework agnostic — React, Vue, Angular, Svelte, or vanilla JS
- GDPR by default — shows accept/reject to all users
- Flexible modes — minimal mode available via
forceEU: false - TypeScript — full type definitions included
- Well-tested — 319 tests, TDD approach
- CSS Encapsulation — Web Components with Shadow DOM (v2.0)
- GDPR, CCPA, LGPD — legally compliant worldwide
- WCAG 2.1 AA — keyboard navigation, screen readers, 44px touch targets
- Secure — CSS sanitization, input validation, CSP nonce support
<script src="https://unpkg.com/smallest-cookie-banner@2/dist/cookie-banner.min.js"></script>npm install smallest-cookie-banner// ES Module
import 'smallest-cookie-banner';
// Or with types
import { createCookieBanner, CookieBannerConfig } from 'smallest-cookie-banner';import { useEffect } from 'react';
import 'smallest-cookie-banner';
function App() {
useEffect(() => {
window.CookieBannerConfig = {
onAccept: () => console.log('Accepted'),
onReject: () => console.log('Rejected')
};
}, []);
return <div>Your app</div>;
}<script setup>
import 'smallest-cookie-banner';
window.CookieBannerConfig = {
msg: 'We use cookies.',
onAccept: () => loadAnalytics()
};
</script>// app.component.ts
import 'smallest-cookie-banner';
ngOnInit() {
(window as any).CookieBannerConfig = {
onAccept: () => this.analyticsService.init()
};
}| Mode | Config | Behavior |
|---|---|---|
| GDPR (default) | {} or forceEU: true |
Shows Accept + Reject buttons |
| Minimal | forceEU: false |
Shows OK button |
Note: GDPR mode is the default. All users see accept/reject buttons unless you explicitly set forceEU: false.
interface CookieBannerConfig {
// Text (i18n)
msg?: string; // Banner message
acceptText?: string; // Accept button text
rejectText?: string; // Reject button text (EU only)
// Behavior
days?: number; // Cookie expiry (1-3650, default: 365)
forceEU?: boolean; // GDPR mode (default: true)
autoAcceptDelay?: number; // Auto-accept delay in ms (0-300000)
cookieName?: string; // Cookie name (default: "cookie_consent")
cookieDomain?: string; // Cookie domain for subdomains
// Callbacks
onAccept?: () => void; // Called on accept
onReject?: () => void; // Called on reject
// Styling
style?: string; // Inline styles
css?: string; // Additional CSS
// Security
cspNonce?: string; // CSP nonce for inline styles
container?: HTMLElement; // Custom container
}// English (default)
window.CookieBannerConfig = {
msg: 'We use cookies to enhance your experience.',
acceptText: 'Accept',
rejectText: 'Decline'
};
// Dutch
window.CookieBannerConfig = {
msg: 'Wij gebruiken cookies om uw ervaring te verbeteren.',
acceptText: 'Accepteren',
rejectText: 'Weigeren'
};
// German
window.CookieBannerConfig = {
msg: 'Diese Website verwendet Cookies.',
acceptText: 'Akzeptieren',
rejectText: 'Ablehnen'
};
// Spanish
window.CookieBannerConfig = {
msg: 'Usamos cookies para mejorar tu experiencia.',
acceptText: 'Aceptar',
rejectText: 'Rechazar'
};
// Chinese (Simplified)
window.CookieBannerConfig = {
msg: '我们使用cookies来提升您的体验。',
acceptText: '接受',
rejectText: '拒绝'
};
// Japanese
window.CookieBannerConfig = {
msg: 'このサイトはクッキーを使用しています。',
acceptText: '同意する',
rejectText: '拒否する'
};:root {
--ckb-bg: #222;
--ckb-color: #fff;
--ckb-btn-bg: #fff;
--ckb-btn-color: #222;
--ckb-btn-radius: 4px;
--ckb-padding: 12px 16px;
--ckb-font: 14px system-ui, sans-serif;
--ckb-z: 9999;
}/* Top */
:root { --ckb-bottom: auto; --ckb-top: 0; }
/* Corner toast */
:root { --ckb-bottom: 20px; --ckb-right: 20px; --ckb-left: auto; }
#ckb { width: 320px; border-radius: 8px; }Use the live configurator to customize and generate code.
// Check consent status
CookieBanner.ok // true | false | null
// Programmatic control
CookieBanner.yes() // Accept
CookieBanner.no() // Reject
CookieBanner.reset() // Clear & reloadImportant: The library manages consent state but doesn't block scripts automatically. You must use one of these approaches:
import { createCookieBanner, loadOnConsent } from 'smallest-cookie-banner';
// 1. Register scripts BEFORE creating banner (they won't load yet)
loadOnConsent('analytics', 'https://www.googletagmanager.com/gtag/js?id=G-XXXXX');
loadOnConsent('marketing', 'https://connect.facebook.net/en_US/fbevents.js');
// 2. Create banner - scripts load automatically when user consents
createCookieBanner({ mode: 'gdpr', forceEU: true });What happens:
- User clicks "Accept All" → Both scripts load
- User clicks "Reject All" → No scripts load
- User enables only Analytics → Only analytics script loads
<!-- Mark scripts as blocked with data attributes -->
<script type="text/plain" data-consent="analytics" data-src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX"></script>
<script type="text/plain" data-consent="marketing" data-src="https://connect.facebook.net/en_US/fbevents.js"></script>
<script type="module">
import { createCookieBanner, blockScriptsUntilConsent } from 'smallest-cookie-banner';
// Scan DOM for blocked scripts
blockScriptsUntilConsent();
// Banner handles the rest
createCookieBanner({ mode: 'gdpr', forceEU: true });
</script>import { createCookieBanner, loadOnConsent } from 'smallest-cookie-banner';
// Basic usage
loadOnConsent('analytics', 'https://example.com/analytics.js');
// With callback (runs after script loads)
loadOnConsent('analytics', 'https://example.com/script.js', () => {
console.log('Script loaded!');
});
// Works with custom cookie names - reads from window.CookieBannerConfig
window.CookieBannerConfig = { cookieName: 'my_consent' };
loadOnConsent('analytics', 'https://example.com/analytics.js'); // Uses 'my_consent'Note: loadOnConsent automatically reads cookieName from window.CookieBannerConfig if set. This works on return visits even before createCookieBanner is called.
import { createCookieBanner } from 'smallest-cookie-banner';
createCookieBanner({
mode: 'gdpr',
forceEU: true,
onConsent: (consent) => {
// consent = { essential: true, analytics: true/false, marketing: true/false, functional: true/false }
if (consent.analytics) {
// Load Google Analytics
const script = document.createElement('script');
script.src = 'https://www.googletagmanager.com/gtag/js?id=G-XXXXX';
document.head.appendChild(script);
}
if (consent.marketing) {
// Load Facebook Pixel, etc.
}
}
});// Set defaults BEFORE gtag loads
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
'analytics_storage': 'denied',
'ad_storage': 'denied',
});
// Update on consent
createCookieBanner({
mode: 'gdpr',
forceEU: true,
onConsent: (consent) => {
gtag('consent', 'update', {
'analytics_storage': consent.analytics ? 'granted' : 'denied',
'ad_storage': consent.marketing ? 'granted' : 'denied',
});
}
});<!DOCTYPE html>
<html>
<head>
<title>My Site</title>
</head>
<body>
<h1>Welcome to My Site</h1>
<!-- Cookie Banner -->
<script type="module">
import {
createCookieBanner,
loadOnConsent
} from 'https://unpkg.com/smallest-cookie-banner@2/dist/cookie-banner.js';
// Step 1: Register scripts with their consent categories
// These will NOT load until user consents to that category
// Analytics scripts
loadOnConsent('analytics', 'https://www.googletagmanager.com/gtag/js?id=G-XXXXX', () => {
// Callback runs after script loads
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXX');
});
// Marketing scripts
loadOnConsent('marketing', 'https://connect.facebook.net/en_US/fbevents.js');
// Functional scripts (chat widget, etc.)
loadOnConsent('functional', 'https://example.com/chat-widget.js');
// Step 2: Create the banner with custom categories
const banner = createCookieBanner({
mode: 'gdpr',
forceEU: true, // Show to all users, not just EU
// Custom category descriptions (optional)
categories: [
{
id: 'essential',
name: 'Essential',
description: 'Required for the site to work',
required: true
},
{
id: 'analytics',
name: 'Analytics',
description: 'Google Analytics - helps us improve the site'
},
{
id: 'marketing',
name: 'Marketing',
description: 'Facebook Pixel - for targeted ads'
},
{
id: 'functional',
name: 'Functional',
description: 'Live chat and support widgets'
},
],
// Customize text
msg: 'We use cookies to enhance your experience.',
acceptText: 'Accept All',
rejectText: 'Reject All',
// Privacy policy link
privacyPolicyUrl: '/privacy',
// Show widget to change preferences later
widget: { enabled: true, position: 'bottom-left' },
// Optional: Get notified of consent changes
onConsent: (consent, record) => {
console.log('User consent:', consent);
// consent = { essential: true, analytics: true, marketing: false, functional: true }
// Optional: Send to your server for audit trail
// fetch('/api/consent', { method: 'POST', body: JSON.stringify(record) });
}
});
</script>
</body>
</html>What users see:
- Banner appears with message and category checkboxes
- User can toggle: Analytics ☑️, Marketing ☐, Functional ☑️
- User clicks "Accept All", "Reject All", or custom selection
- Only consented scripts load
- Small widget appears (bottom-left) to change preferences later
Full type definitions included:
import {
createCookieBanner,
CookieBannerConfig,
CookieBannerInstance
} from 'smallest-cookie-banner';
const config: CookieBannerConfig = {
msg: 'We use cookies.',
onAccept: () => loadAnalytics()
};
const banner: CookieBannerInstance = createCookieBanner(config);| Library | Size |
|---|---|
| smallest-cookie-banner | ~6KB |
| cookie-consent | ~15KB |
| cookieconsent | ~25KB |
| tarteaucitron | ~45KB |
| OneTrust | ~100KB+ |
| Region | Law | Status |
|---|---|---|
| EU | GDPR | ✅ |
| California | CCPA | ✅ |
| Brazil | LGPD | ✅ |
| UK | UK GDPR | ✅ |
| Canada | PIPEDA | ✅ |
- Keyboard navigation (Tab, Escape)
- Focus trap while visible
- ARIA attributes (
role="dialog",aria-modal) - 44px touch targets
- Respects
prefers-reduced-motion
- CSS injection protection
- Input validation
- CSP nonce support
SameSite=LaxcookiesSecureflag on HTTPS
Contributions welcome! Current version: v1.0.6
- Fork the repo and clone locally
- Install dependencies:
npm install - Create a feature branch:
git checkout -b feature/your-feature
All PRs must include:
| Type | Requirements |
|---|---|
| Bug Fix | Test case reproducing the bug + fix |
| New Feature | Tests covering the feature, updated types |
| Refactor | No coverage regression, passing tests |
| Docs | Accurate, clear, spell-checked |
- Tests pass:
npm test - 90%+ code coverage (enforced by CI)
- Linting passes:
npm run lint - Types check:
npm run typecheck - Build succeeds:
npm run build - PR description explains the change
All PRs are automatically checked for:
- Linting (ESLint + TypeScript)
- Tests (Jest, 319 test cases)
- Coverage threshold (90% minimum)
- Build verification
npm install
npm test # Run tests with coverage
npm run build # Build for production
npm run lint # Check code styleChrome 60+, Firefox 60+, Safari 12+, Edge 79+, iOS Safari 12+, Chrome Android 70+
MIT