How to Build a Self-Hosted Cookie Banner (No External CMP) Using localStorage + Conditional Tracking Injection

Cookie banners are one of those modern web chores: boring, important, and legally spicy. Many sites outsource consent management to external platforms (CMPs), but you can absolutely build a self-hosted cookie banner that keeps the essential functionality: store user choices, block tracking by default, and load analytics scripts only when the user opts in.

Share:

This guide shows a practical approach using vanilla JavaScript and localStorage. We'll store consent as a small JSON object, then inject tracking code only if consent is enabled (and skip injection if it's disabled).

Note: This is a technical implementation guide, not legal advice. Laws and regulator expectations vary by country and can change over time. Still, most guidance converges on a few core ideas: clear information, opt-in for non-essential trackers, and the ability to withdraw consent easily.

Why Build Your Own Cookie Banner?

Pros (Control, Performance, No Vendor Lock-in)

Cons (Maintenance, Legal Accuracy, Testing Burden)

What "Consent" Usually Needs to Look Like

Most cookie rules treat "cookies and similar technologies" broadly (not just cookies: localStorage, pixels, SDKs, etc.). The recurring expectations are:

Essential vs Analytics vs Marketing

Withdraw / Change Consent Must Be Easy

Regulations emphasize that users must be able to withdraw consent as easily as they gave it. This means:

Reject Should Be as Accessible as Accept

A common enforcement theme in Europe: refusing should not be "hidden" behind extra friction. We'll reflect this by offering:

Architecture: How the Solution Works

Data Model Stored in localStorage

We store consent preferences as a JSON object in localStorage:

{
  "version": "1.0",
  "timestamp": "2026-01-14T10:15:00.000Z",
  "preferences": {
    "essential": true,
    "analytics": false,
    "marketing": false
  }
}

Default Behavior: Block Tracking Until Consent

No consent stored = do not load analytics/marketing scripts. This is the most important rule for GDPR compliance.

Conditional Script Injection

Scripts are loaded dynamically based on consent:

"Manage Preferences" UI + Persistent "Cookie Settings" Link

Users can access cookie settings at any time via:

Step-by-Step Implementation (HTML + CSS + JS)

Step 1 — Add the Banner Markup

Create a file sections/cookie-banner.html:

<!-- Cookie Consent Banner -->
<div id="cookie-banner" class="cookie-banner" style="display: none;">
    <div class="cookie-banner-content">
        <!-- Titolo e Reject All in alto a sinistra -->
        <div class="cookie-banner-header">
            <i class="ph ph-cookie"></i>
            <h3>Your Privacy Matters</h3>
            <button id="cookie-reject-btn" class="cookie-banner-reject-link">
                Reject All
            </button>
        </div>
        
        <!-- Bottoni in alto a destra -->
        <div class="cookie-banner-actions">
            <button id="cookie-settings-btn" class="cookie-btn cookie-btn-secondary">
                <i class="ph ph-gear"></i> Manage Preferences
            </button>
            <button id="cookie-accept-btn" class="cookie-btn cookie-btn-primary">
                Accept All
            </button>
        </div>
        
        <!-- Descrizione in basso a sinistra -->
        <div class="cookie-banner-body">
            <p>We use cookies and similar technologies to enhance your browsing experience, 
            analyze website traffic, and deliver personalized advertising. By clicking "Accept All", 
            you consent to our use of cookies.</p>
        </div>
        
        <!-- Link policy in basso a destra -->
        <p class="cookie-banner-links">
            <a href="/privacy-policy.html">Privacy Policy</a> • 
            <a href="/cookie-policy.html">Cookie Policy</a>
        </p>
    </div>
</div>

<!-- Cookie Manage Widget - Fixed bottom left -->
<button id="cookie-manage-widget" class="cookie-manage-widget" 
        aria-label="Manage Cookie Preferences" title="Cookie Settings">
    <i class="ph ph-cookie"></i>
</button>

Step 2 — Add Styling

The banner uses a modern grid layout with responsive design. Key features:

Create css/cookie-banner.css with the complete styling (see full implementation).

Step 3 — Implement Consent Storage in localStorage

The CookieConsent class manages all consent operations:

class CookieConsent {
    constructor() {
        this.CONSENT_KEY = 'cookie_consent';
        this.CONSENT_VERSION = '1.0';
        this.consent = this.loadConsent();
        this.init();
    }

    loadConsent() {
        try {
            const stored = localStorage.getItem(this.CONSENT_KEY);
            return stored ? JSON.parse(stored) : null;
        } catch (e) {
            console.error('Error loading cookie consent:', e);
            return null;
        }
    }

    saveConsent(preferences) {
        const consent = {
            version: this.CONSENT_VERSION,
            timestamp: new Date().toISOString(),
            preferences: preferences
        };
        
        try {
            localStorage.setItem(this.CONSENT_KEY, JSON.stringify(consent));
            this.consent = consent;
        } catch (e) {
            console.error('Error saving cookie consent:', e);
        }
    }
}

Step 4 — Inject Analytics Scripts Only If Enabled

This is the critical part for GDPR compliance. Scripts are loaded only after user consent:

loadGoogleAnalytics() {
    // Check if already loaded
    if (window.gtag) {
        console.log('Google Analytics already loaded');
        return;
    }

    // Load Google Analytics
    const script = document.createElement('script');
    script.async = true;
    script.src = 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX';
    document.head.appendChild(script);

    script.onload = () => {
        window.dataLayer = window.dataLayer || [];
        function gtag(){dataLayer.push(arguments);}
        window.gtag = gtag;
        gtag('js', new Date());
        gtag('config', 'G-XXXXXXX', {
            'anonymize_ip': true,
            'cookie_flags': 'SameSite=None;Secure'
        });
        console.log('Google Analytics loaded with consent');
    };
}

loadMetaPixel() {
    // Check if already loaded
    if (window.fbq) {
        console.log('Meta Pixel already loaded');
        return;
    }

    // Load Meta Pixel
    !function(f,b,e,v,n,t,s)
    {if(f.fbq)return;n=f.fbq=function(){n.callMethod?
    n.callMethod.apply(n,arguments):n.queue.push(arguments)};
    if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
    n.queue=[];t=b.createElement(e);t.async=!0;
    t.src=v;s=b.getElementsByTagName(e)[0];
    s.parentNode.insertBefore(t,s)}(window, document,'script',
    'https://connect.facebook.net/en_US/fbevents.js');
    
    window.fbq('init', 'XXXXXXXXX');
    window.fbq('track', 'PageView');
    
    console.log('Meta Pixel loaded with consent');
}

Step 5 — Add Settings Button + Allow Changes Later

Users must be able to change their preferences at any time. We provide two access points:

  1. Fixed widget: A floating cookie icon in the bottom-left corner
  2. Footer link: "Cookie Settings" link on the Cookie Policy page
// Cookie manage widget (fixed button)
const manageWidget = document.getElementById('cookie-manage-widget');
if (manageWidget) {
    manageWidget.addEventListener('click', () => {
        this.showModal();
    });
}

Step 6 — Optional: Best-Effort Cookie Cleanup on Withdrawal

When users revoke consent, we attempt to clean up tracking cookies:

disableGoogleAnalytics() {
    // Set opt-out flag
    window['ga-disable-G-XXXXXXX'] = true;
    
    // Delete GA cookies
    this.deleteCookie('_ga');
    this.deleteCookie('_gid');
    this.deleteCookie('_gat');
    this.deleteCookie('_ga_XXXXXXX');
    
    console.log('Google Analytics disabled');
}

deleteCookie(name) {
    // Delete cookie for current domain
    document.cookie = name + '=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
    
    // Try to delete for parent domain as well
    const domain = window.location.hostname;
    document.cookie = name + '=; Path=/; Domain=' + domain + '; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
    
    // Try with dot prefix
    document.cookie = name + '=; Path=/; Domain=.' + domain + '; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
}

Important: After changing preferences, the page automatically reloads to ensure all tracking scripts are properly loaded or removed.

Testing Checklist (DevTools)

Verify localStorage

  1. Open DevTools → Application → Local Storage
  2. Look for the cookie_consent key
  3. Verify the JSON structure matches expectations

Verify Scripts Are Not Loaded Pre-Consent

  1. Open site in incognito mode
  2. Before clicking anything, check Network tab
  3. Verify NO requests to googletagmanager.com or facebook.net
  4. Check Elements tab: no analytics <script> tags

Verify Toggles and Persistence

  1. Click "Accept All" → Reload page
  2. Banner should NOT appear (consent saved)
  3. Analytics requests should be present in Network tab
  4. Click cookie widget → Disable analytics → Save
  5. After reload: NO analytics requests

Verify "Withdrawal Is as Easy as Consent"

  1. Ensure cookie widget is always visible
  2. Click widget → Modal opens immediately
  3. Toggle any preference → Save
  4. Changes applied after reload

Common Mistakes (And How to Avoid Them)

Loading GA/GTM in HTML by default
Tracking fires before consent
Inject scripts only after opt-in
"Reject" hidden behind extra clicks
Creates friction and legal risk
Put Reject next to Accept (first layer)
No easy way to change consent
Withdrawal must be easy
Add persistent "Cookie Settings" widget
No versioning
Old consent may not match new categories
Add version and re-prompt when changed
Not reloading after consent changes
Scripts remain loaded in memory
Force page reload after saving preferences
Banner appears on every page load
Consent not properly stored or checked
Check localStorage before showing banner

FAQ

Do analytics cookies always require consent?

In many jurisdictions, yes—analytics are usually not considered "strictly necessary" unless they are completely anonymous and don't track users across sites.

Is localStorage okay for storing consent?

Technically it's still "storing/accessing information on a device." It's commonly used to remember consent choices, but confirm your compliance approach and document it clearly in your privacy policy.

Do I need a "Reject all" button?

Often recommended as a best practice because enforcement actions frequently focus on making refusal easy and not more difficult than acceptance. The "Reject all" button should be as prominent as "Accept all".

What happens if I change the consent categories?

Increment the CONSENT_VERSION in your code. Users with old consent versions will be re-prompted with the banner to make a new choice.

Should I reload the page after consent changes?

Yes. While you can disable tracking via opt-out flags, the cleanest approach is to reload the page so scripts are either loaded or not loaded based on the new preferences.

Conclusion + Next Steps

A self-hosted cookie banner can be clean, fast, and fully functional—if you follow one core rule: no tracking loads until the user opts in. localStorage gives you a simple way to persist choices, and conditional script injection keeps your tracking behavior honest.

Next Steps:

Want help implementing a custom cookie consent solution for your business? Get in touch and let's build something compliant and user-friendly together.