Tutorials 3 min read

How to create a dismissible cookie banner with Tailwind CSS: JavaScript and Alpine.js

Build an accessible, dismissible cookie banner with Tailwind CSS, two ways: vanilla JavaScript or Alpine.js, with focus management for keyboard users.

Cookies, here we come! Today we are building a dismissible cookie banner, twice: first with vanilla JavaScript, then with Alpine.js, so you can pick whichever fits your stack.

A cookie banner is the little dialog that tells visitors your site uses cookies and asks for their consent. It’s more than a formality: privacy laws like GDPR and CCPA make it a legal necessity, and a clear, accessible banner builds trust by letting users make informed choices about their data.

Use cases

  • Educating users: explain in plain language how their data is collected and used.
  • Legal compliance: meet GDPR, CCPA and other privacy laws that require consent.
  • Empowering choices: let visitors distinguish essential from non-essential cookies.
  • Building trust: a clear, user-friendly banner leaves a better impression and can even help conversion.

The markup

  • id="cookie-banner": the id of the cookie banner.
  • role="dialog": tells assistive technology the banner is a dialog window.
  • aria-labelledby="cookieBannerTitle" and aria-describedby="cookieBannerDesc": link the dialog to its title and description for screen readers.
  • id="accept-cookies": the button JavaScript uses to dismiss the banner.
html
<div
  id="cookie-banner"
  role="dialog"
  aria-labelledby="cookieBannerTitle"
  aria-describedby="cookieBannerDesc"
>
  <!-- Cookie banner content container -->
  <div>
    <img src="..." alt="Cookies" />
    <h2>Our website uses cookies</h2>
    <p>
      Our website uses cookies. By continuing, we assume your permission to
      deploy cookies as detailed in our <a href="#">Privacy Policy.</a>
    </p>
    <!-- Accept cookies button -->
    <div class="mt-8">
      <button id="accept-cookies">Accept all cookies</button>
    </div>
    <!-- Manage cookies link -->
    <div>
      <a href="#">Manage cookies</a>
    </div>
  </div>
</div>

The script

  • document.addEventListener("DOMContentLoaded", () => {: waits for the page, then grabs the banner and the accept button.
  • acceptButton.focus();: sets focus on the accept button as soon as the banner is displayed, which keyboard users will thank you for.
  • acceptButton.addEventListener("click", function () {: hides the banner with cookieBanner.style.display = "none"; when the button is clicked.
js
document.addEventListener("DOMContentLoaded", function () {
  const cookieBanner = document.getElementById("cookie-banner");
  const acceptButton = document.getElementById("accept-cookies");

  // Set focus on the accept button once the banner is displayed
  acceptButton.focus();

  // Hide the cookie banner when the accept button is clicked
  acceptButton.addEventListener("click", function () {
    cookieBanner.style.display = "none";
  });
});

The Alpine.js version

Same component, no separate script: the state lives in x-data and the focus management happens right in the markup.

  • x-data="{ showAlert: true }": initializes the component’s state.
  • x-init="$nextTick(() => { if (showAlert) $refs.firstButton.focus() })": focuses the “Accept all cookies” button as soon as the banner shows, for keyboard navigation.
  • x-show="showAlert": controls the visibility of the banner based on user consent.
  • x-ref="firstButton" with @click="showAlert = false": dismisses the banner.
html
<div
  x-data="{ showAlert: true }"
  x-init="$nextTick(() => { if (showAlert) $refs.firstButton.focus() })"
  x-show="showAlert"
  role="dialog"
  aria-labelledby="cookieBannerTitle"
  aria-describedby="cookieBannerDesc"
  class="..."
>
  <div class="...">
    <img src="..." alt="Cookies" class="..." />
    <h2 id="cookieBannerTitle" class="...">...</h2>
    <p id="cookieBannerDesc" class="...">...</p>
    <button x-ref="firstButton" @click="showAlert = false" class="...">
      ...
    </button>
    <a href="#_" class="...">...</a>
  </div>
</div>

You can try the Alpine version separately: live demo and source code.

Which one should you use?

If Alpine is already on the page, x-show and x-init handle visibility and focus with no script at all. If not, the vanilla version is a handful of lines and no dependency.

Conclusion

Whichever flavor you pick, keep the banner accessible: clear, jargon-free text, keyboard navigation, and content screen readers can interpret. It’s about making the internet a welcoming place for all.

Hope you enjoyed this tutorial and have a great day!

/Michael Andreuzza