smallest-cookie-banner
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!
Features
Minimal
- ~7KB gzipped — still smaller than most images
- Zero dependencies — no bloat, no supply chain risk
- No external requests — works offline, no tracking
Flexible
- 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
Smart
- 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)
Compliant & Accessible
- 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
Quick Start
CDN (Vanilla JS)
<script src="https://unpkg.com/smallest-cookie-banner@2/dist/cookie-banner.min.js"></script>
npm (Any Framework)
npm install smallest-cookie-banner
// ES Module import 'smallest-cookie-banner'; // Or with types import { createCookieBanner, CookieBannerConfig } from 'smallest-cookie-banner';
React
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>; }
Vue
<script setup> import 'smallest-cookie-banner'; window.CookieBannerConfig = { msg: 'We use cookies.', onAccept: () => loadAnalytics() }; </script>
Angular
// app.component.ts import 'smallest-cookie-banner'; ngOnInit() { (window as any).CookieBannerConfig = { onAccept: () => this.analyticsService.init() }; }
How It Works
| 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.
Configuration
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 }
i18n Examples
// 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: '拒否する' };
Styling
CSS Variables
: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; }
Position
/* 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; }
Visual Configurator
Use the live configurator to customize and generate code.
API
// Check consent status CookieBanner.ok // true | false | null // Programmatic control CookieBanner.yes() // Accept CookieBanner.no() // Reject CookieBanner.reset() // Clear & reload
Script Blocking
Important: The library manages consent state but doesn't block scripts automatically. You must use one of these approaches:
Quick Start (Recommended)
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
HTML Approach (No JS Changes)
<!-- 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>
loadOnConsent API
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.
Callback Approach (Full Control)
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. } } });
Google Consent Mode v2
// 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', }); } });
Complete Example with Custom Categories
<!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
TypeScript
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);
Size Comparison
| Library | Size |
|---|---|
| smallest-cookie-banner | ~6KB |
| cookie-consent | ~15KB |
| cookieconsent | ~25KB |
| tarteaucitron | ~45KB |
| OneTrust | ~100KB+ |
Compliance
| Region | Law | Status |
|---|---|---|
| EU | GDPR | ✅ |
| California | CCPA | ✅ |
| Brazil | LGPD | ✅ |
| UK | UK GDPR | ✅ |
| Canada | PIPEDA | ✅ |
Accessibility (WCAG 2.1 AA)
- Keyboard navigation (Tab, Escape)
- Focus trap while visible
- ARIA attributes (
role="dialog",aria-modal) - 44px touch targets
- Respects
prefers-reduced-motion
Security
- CSS injection protection
- Input validation
- CSP nonce support
SameSite=LaxcookiesSecureflag on HTTPS
Contributing
Contributions welcome! Current version: v1.0.6
Getting Started
- Fork the repo and clone locally
- Install dependencies:
npm install - Create a feature branch:
git checkout -b feature/your-feature
PR Requirements
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 |
Checklist
- 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
CI Pipeline
All PRs are automatically checked for:
- Linting (ESLint + TypeScript)
- Tests (Jest, 319 test cases)
- Coverage threshold (90% minimum)
- Build verification
Development
npm install npm test # Run tests with coverage npm run build # Build for production npm run lint # Check code style
Browser Support
Chrome 60+, Firefox 60+, Safari 12+, Edge 79+, iOS Safari 12+, Chrome Android 70+
License
MIT