GDPR Compliant Cookie Consent Banner with Alpine.js
AI generated
x-data
Alpine
Alpine.js · GDPR · Privacy · Consent
GDPR Compliant Cookie Consent Banner with Alpine.js
Separate categories, gate script loading, allow withdrawal any time

A cookie consent banner decides directly whether tracking scripts, marketing pixels and third party embeds are allowed to load at all. With Alpine.js you build a cookie consent banner that separates categories cleanly, persists consent, only loads scripts after consent has been granted, and allows withdrawal at any time without a page reload.

19 min read x-data · localStorage · x-if · Alpine store Alpine.js 3.x

1. Why a cookie consent banner must legally be more than a notice

A cookie consent banner is not merely a design question, it is a legally binding building block of the website. Under GDPR and comparable national laws, every website that uses non essential cookies or tracking scripts needs an informed, active and revocable consent before these scripts are loaded. A cookie consent banner that only offers a close button without a real choice does not fulfill this requirement, even if it looks professional.

The central difference from a simple notice banner: a legally compliant cookie consent banner must offer granular categories, make rejection just as easy as consent, and may only load scripts after explicit consent, not beforehand with subsequent blocking. That exact before and after behavior is the hardest technical part, and this is precisely where Alpine.js helps, because it connects reactive state directly to the DOM without requiring a heavy consent management framework.

The following sections build a complete cookie consent banner that separates categories, persists consent in the browser, gates scripts on load, and lets the user change their decision at any time without a page reload.

2. Categories instead of all or nothing: the banner's state

A legally sound cookie consent banner distinguishes at least three categories: necessary cookies that may load without consent, statistics cookies for analytics tools, and marketing cookies for tracking pixels and retargeting. Each category needs its own boolean value in the x-data object, with the necessary category fixed to true and not deactivatable, which should also be made visually clear in the markup, for example via a disabled checkbox.

In addition to the actual consent state, a cookie consent banner needs a visibility state that distinguishes whether a decision has already been made at all. Without this separation the banner would reappear on every page visit, even if the user had already decided. The visibility state is checked against the stored consent when the page loads, and the banner only appears if no valid decision exists yet.


// cookieConsent.js — Alpine.data component for the consent banner
document.addEventListener('alpine:init', () => {
  Alpine.data('cookieConsent', () => ({
    visible: false,
    settingsOpen: false,
    consent: {
      necessary: true,   // always true, not user-configurable
      statistics: false,
      marketing: false
    },

    init() {
      const stored = this.loadConsent();
      if (stored) {
        this.consent = { ...this.consent, ...stored };
        this.applyConsent();
      } else {
        // No decision on file yet — show the banner
        this.visible = true;
      }
    }
  }));
});

3. Persisting consent: localStorage and an expiry date

A user's decision must persist across sessions, otherwise the cookie consent banner reappears on every visit, which users rightly perceive as intrusive. localStorage is the right storage location for this, because unlike a session cookie it survives browser restarts. It is important to store a timestamp alongside the decision itself, so that consent automatically expires after a defined period, typically twelve months, and the cookie consent banner is shown again.

Besides the expiry date, a version number of the consent configuration also belongs in the stored record. If the list of categories or services in use changes, for instance because a new marketing tool is added, the cookie consent banner must reappear even if the previous consent has not yet expired. A simple version comparison on load decides whether the stored consent is still valid.


const CONSENT_VERSION = 2;
const CONSENT_KEY = 'cookie_consent_v1';
const CONSENT_TTL_DAYS = 365;

function loadConsent() {
  const raw = localStorage.getItem(CONSENT_KEY);
  if (!raw) return null;

  const parsed = JSON.parse(raw);
  const ageInDays = (Date.now() - parsed.timestamp) / 86400000;

  // Expired or outdated config version — treat as no decision
  if (ageInDays > CONSENT_TTL_DAYS || parsed.version !== CONSENT_VERSION) {
    return null;
  }
  return parsed.consent;
}

function saveConsent(consent) {
  localStorage.setItem(CONSENT_KEY, JSON.stringify({
    version: CONSENT_VERSION,
    timestamp: Date.now(),
    consent
  }));
}

4. Gated script loading: activating scripts only after consent

The technically most critical part of a cookie consent banner is the actual gating of scripts. Analytics and marketing scripts must not sit in the HTML as a regular <script src="…"> tag, because the browser loads that immediately while parsing, regardless of the consent decision. Instead the script tag is disabled with type="text/plain" and only activated dynamically via JavaScript once the matching category has been agreed to in the cookie consent banner.

Alpine.js takes on the role of mediator between consent state and script activation here. A watch on the relevant category in the x-data object triggers a function on the transition from false to true that creates the corresponding script element and inserts it into the DOM. If a category is later withdrawn, it must additionally be ensured that already set cookies in that category are actively removed, because merely not loading new scripts is not enough for a compliant cookie consent banner.


Alpine.data('cookieConsent', () => ({
  consent: { necessary: true, statistics: false, marketing: false },

  init() {
    // React to any change in consent state — activate or deactivate scripts
    this.$watch('consent.statistics', (value) => {
      value ? this.loadScriptsForCategory('statistics') : this.purgeCookiesForCategory('statistics');
    });
    this.$watch('consent.marketing', (value) => {
      value ? this.loadScriptsForCategory('marketing') : this.purgeCookiesForCategory('marketing');
    });
  },

  loadScriptsForCategory(category) {
    // Activate all gated <script type="text/plain" data-category="…"> tags
    document.querySelectorAll(`script[type="text/plain"][data-category="${category}"]`)
      .forEach((placeholder) => {
        const script = document.createElement('script');
        script.textContent = placeholder.textContent;
        placeholder.replaceWith(script);
      });
  },

  purgeCookiesForCategory(category) {
    // Explicit cookie removal — required, not optional, on withdrawal
    const cookieNames = category === 'statistics' ? ['_ga', '_gid'] : ['_fbp', '_gcl_au'];
    cookieNames.forEach((name) => {
      document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`;
    });
  }
}));

5. A global Alpine store for a consistent consent status

Once several components on the page need to know the consent status, for instance an embedded YouTube video and a marketing widget in the footer, a local x-data state inside the banner alone is no longer enough. A global Alpine store centralizes the cookie consent banner status, so every component on the page can read the same current state, without duplicating state or synchronizing it through events.

The store is initialized once when the page loads and remains the single source of truth for the consent status afterward. Every component that depends on consent, for instance an embedded video, checks Alpine.store('consent').marketing directly instead of keeping its own copy of the state. That prevents a cookie consent banner and a video embed from having different ideas about the current consent status.

6. Withdrawal at any time: the settings panel after the first decision

Another aspect that is easily overlooked in a store based implementation of a cookie consent banner is the initialization order. The store must be registered before any component that accesses it, otherwise Alpine throws an error during page setup, because Alpine.store('consent') is still undefined. In practice this means placing the store call in the layout as early as possible in the head or right at the start of the body, well before embedded videos or other consent dependent widgets.

GDPR explicitly requires that withdrawal must be just as easy as the original consent. A cookie consent banner that vanishes completely after the first decision and offers no way back violates this requirement. The solution is a permanently visible, unobtrusive link or button, usually placed in the footer, that reopens the banner's settings panel at any time.

Technically this is the same component as on the first visit, just with a different trigger. Instead of appearing automatically when consent is missing, the cookie consent banner is opened here via an explicit click, but shows the previously stored settings pre filled. After a change, the new decision is stored again with a current timestamp and version, exactly as on the first visit.


// Reopen the same banner component via the footer settings link
Alpine.data('cookieConsent', () => ({
  visible: false,
  consent: { necessary: true, statistics: false, marketing: false },

  openSettings() {
    // Pre-fill with the previously stored decision, then show again
    const stored = this.loadConsent();
    if (stored) this.consent = { ...this.consent, ...stored };
    this.visible = true;
  },

  saveAndClose() {
    this.saveConsent(this.consent);
    this.visible = false;
  }
}));

7. Accessibility and focus management in the banner

A cookie consent banner that does not handle focus when it appears is hard to use for keyboard and screen reader users. On opening, focus should be explicitly set on the first interactive element in the banner, usually the accept all button or the link to settings. As long as the banner is visible, focus should be kept inside the banner via a focus trap, so tab navigation does not accidentally jump into the page content behind it.

In addition, the cookie consent banner should be marked up with role="dialog" and aria-modal="true" when it blocks the rest of the page, or with a less intrusive role when it only appears as a bar at the bottom while the page behind it remains usable. Both variants are GDPR compliant, as long as the actual consent logic is correctly implemented, the choice is a pure UX decision.


<!-- Focus trap and dialog semantics for the visible banner -->
<div
  x-data="cookieConsent()"
  x-show="visible"
  x-trap.noscroll="visible"
  role="dialog"
  aria-modal="true"
  aria-label="Cookie settings"
>
  <button x-ref="acceptAll" @click="acceptAll()" autofocus>Accept all</button>
  <button @click="settingsOpen = true">Settings</button>
</div>

8. Proof obligation: logging and versioning consent

In a dispute, a website operator must be able to prove that valid consent existed, when it was given, and which categories were specifically agreed to. A cookie consent banner that only stores the decision in the user's browser is often not enough for this proof obligation alone, especially for larger websites with legal exposure. It is advisable to additionally log the decision optionally and anonymously server side too, for instance with a hash of the user, a timestamp and the selected categories.

Versioning the consent configuration itself, as described in section three, is also part of the proof obligation. When a new category or a new service is introduced, it must be traceable from which point in time which version of the cookie consent banner was active and which users consented under which version.

9. Cookie consent solutions compared

There are several common ways to implement a cookie consent banner, with substantial differences in cost, control and technical integration.

Approach Cost Design control External dependency
Commercial CMP SaaS Monthly license fees Limited by theme constraints Externally hosted script
Static banner without logic None Full Not GDPR compliant
Cookie consent banner with Alpine.js One time development effort Full, custom design None, all in your own code
Google Consent Mode alone No additional cost No visible UI included Complements, does not replace a banner

The comparison shows that a self built cookie consent banner with Alpine.js avoids ongoing SaaS CMP license costs while offering full control over design and loading behavior, without requiring an additional, externally hosted script to be added to the content security policy.

Mironsoft

GDPR compliant Alpine.js components for Magento Hyvä shops

A cookie consent banner without an expensive CMP subscription?

We build custom, legally sound cookie consent banners with Alpine.js for your Hyvä shop, with category separation, gated script loading and full control over the design.

Compliance check

Reviewing existing banners for GDPR and national law compliance

Custom development

Cookie consent banners with Alpine.js, no expensive CMP subscription

Script gating

Loading analytics and marketing scripts only after consent

10. Summary

A GDPR compliant cookie consent banner needs clearly separated categories, a real prior decision instead of subsequent blocking, persistent storage with an expiry date and versioning, and a way to withdraw that is reachable at any time. With Alpine.js such a cookie consent banner comes together without an expensive CMP subscription, yet with full control over design, loading behavior and privacy logic.

Technically decisive is gated script loading: scripts may only be activated after explicit consent, and on withdrawal already set cookies must be actively removed. A global Alpine store keeps the consent status consistent for every component on the page, from embedded videos to marketing widgets. Accessibility and traceable logging round off a production ready cookie consent banner.

GDPR Compliant Cookie Consent Banner — The Essentials at a Glance

Categories

Necessary, statistics, marketing as separate boolean values in the x-data state.

Persistence

localStorage with a timestamp and version number, expiring after twelve months.

Script gating

type="text/plain" placeholder, activated by an Alpine watch only after consent.

Withdrawal

A permanently visible footer link reopens the settings panel at any time.

11. FAQ: GDPR Compliant Cookie Consent Banner with Alpine.js

1Is a close button enough?
No, GDPR requires active, informed consent with an equally easy rejection option.
2How long to store consent?
Commonly twelve months, checked via a timestamp in the stored record.
3Marketing scripts allowed in HTML?
Only as a disabled type=text/plain placeholder, never as an active script src tag.
4Withdrawing a category?
Already set cookies must be actively deleted, merely not reloading is not enough.
5Why a global Alpine store?
Prevents inconsistent states between the banner and other consent dependent components.
6Withdrawal as easy as consent?
Yes, explicit GDPR requirement. A permanently visible footer link fulfills this.
7Focus management needed?
Yes, set focus on the first element and keep it inside the banner via a focus trap.
8localStorage as sole proof?
For higher exposure, additional anonymous server side logging is recommended.
9When to bump the version number?
On every substantive change, for instance a new tool or new category.
10Does Google Consent Mode replace the banner?
No, it only adjusts Google services, it provides no consent interface of its own.