How to build a full-width mega menu with Tailwind CSS and Alpine.js

Craft a responsive, full-width mega menu using Tailwind CSS and Alpine.js with teleportation, backdrop, and dynamic alignment for precise positioning.

Published on November 18, 2025 by Michael Andreuzza

This tutorial shows how to build a full‑width, fixed‑position mega menu using Tailwind CSS for styling and Alpine.js for behavior. The flyout aligns itself under its trigger button, teleports to body for correct stacking, and updates position on scroll/resize to avoid jitter. We also include an overlay (backdrop) that closes the menu on outside click and a small event bus so multiple flyouts can coexist.

We’ll cover:

  • Minimal Alpine state and the align/toggle logic
  • The trigger button and x-ref for measurements
  • The backdrop to close on outside click
  • Teleporting the panel to body and transitions
  • Tips for accessibility and avoiding layout jitter

Prerequisites: Tailwind CSS v3+ (or v4) and Alpine.js v3 must be present on the page. x-teleport ships with Alpine v3. For production, consider binding :aria-expanded="open" on the button and focusing the first interactive element when the menu opens.

1 — Alpine state and alignment

We keep a small x-data object at the wrapper:

  • uid: used to uniquely identify each flyout instance
  • open: whether the flyout is visible
  • style: inline style for the teleported element (left/right/top)
  • gap: pixel gap between the button and the panel
  • align(): measures the trigger and computes fixed coordinates
  • toggle() and close(): interaction helpers; requestAnimationFrame avoids initial placement jitter by aligning first and then revealing

On init, we:

  • Listen for a custom close-all-flyouts event to close other menus
  • Re‑align on resize and scroll when open
  • If opening by default, run align() immediately
html
<!-- Shell with alignment + event bus -->
<div
  x-data="{
    uid: Math.random().toString(36).slice(2),
    open: false,
    style: '',
    gap: 8,
    align() {
      const r = this.$refs.btn.getBoundingClientRect();
      const top = r.bottom + this.gap;
      this.style = `position:fixed; left:0; right:0; top:${top}px;`;
    },
    toggle() {
      if (this.open) { this.open = false; return }
      window.dispatchEvent(new CustomEvent('close-all-flyouts', { detail: this.uid }))
      this.align();
      requestAnimationFrame(() => this.open = true)
    },
    close() { this.open = false },
  }"
  x-init="
    window.addEventListener('close-all-flyouts', (e) => { if (e.detail !== uid) open = false });
    const realign = () => { if (open) align() };
    window.addEventListener('resize', realign);
    window.addEventListener('scroll', realign, { passive: true });
  "
>
  <!-- content -->
</div>

2 — Trigger button

The trigger holds x-ref="btn" so align() can read its bounding box. Keep icons small and be sure to make it keyboard focusable. For best accessibility, bind :aria-expanded="open".

html
<button
  x-ref="btn"
  @click.stop="toggle"
  :aria-expanded="open"
  class="relative flex items-center gap-2 h-9 px-4 rounded-md text-sm text-zinc-600 bg-zinc-50 outline outline-zinc-100 hover:bg-zinc-200 focus-visible:outline-2 focus-visible:outline-zinc-600"
>
  <span>Menu</span>
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
    <path d="M6 9l6 6l6 -6"/>
  </svg>
  <span class="sr-only">Open mega menu</span>
  <!-- sr-only ensures better a11y for icon button variants -->
</button>

3 — Backdrop (outside click)

A fixed, full‑screen backdrop closes the menu when clicked. We mount it only when open is true and fade it with x-transition.opacity.

html
<div
  x-show="open"
  x-transition.opacity
  class="fixed inset-0 z-[999]"
  @click="close"
  aria-hidden="true"
  style="display:none"
></div>

4 — Teleported flyout

We render the panel using <template x-teleport="body"> so it escapes any overflow/stacking contexts near the trigger and sits at the top document layer. We apply position:fixed; left:0; right:0; top:… to span full width and keep the computed top offset in sync with the trigger.

html
<template x-teleport="body">
  <div
    x-show="open"
    :style="style"
    class="z-[1000] bg-white shadow-lg outline outline-zinc-900/5"
    @keydown.escape.window="close"
    x-transition:enter="transition ease-out duration-200"
    x-transition:enter-start="opacity-0 -translate-y-1"
    x-transition:enter-end="opacity-100 translate-y-0"
    x-transition:leave="transition ease-in duration-150"
    x-transition:leave-start="opacity-100 translate-y-0"
    x-transition:leave-end="opacity-0 -translate-y-1"
    style="display:none"
  >
    <div class="px-8 py-8 mx-auto max-w-7xl md:px-12 lg:px-24">
      <!-- Your grid/content goes here -->
    </div>
  </div>
  <!-- style on the wrapper is computed by align() -->
</template>

5 — Accessibility and polish

  • Keyboard: close on Escape with @keydown.escape.window="close".
  • ARIA: Consider :aria-expanded="open" on the trigger and role="menu"/aria-labelledby where appropriate.
  • Motion: use Tailwind transitions for subtle opacity/translate-y effects.
  • Z‑index: the backdrop and panel use high z‑indices to appear above content.
html
<!-- Small a11y touches on the trigger and panel -->
<button :aria-expanded="open" aria-controls="mega-menu">Open</button>
<div id="mega-menu" role="region" aria-label="Main mega menu" @keydown.escape.window="close"></div>

<!-- Optional: reduce motion for sensitive users -->
<style>
  @media (prefers-reduced-motion: reduce) {
    .transition { transition: none !important; }
  }
  .sr-only {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
  }
</style>

Full snippet

Paste the full example below into a page that loads Tailwind CSS and Alpine.js. It opens by default so you can see the layout; set open: false in x-data to start closed.

html
<div
  class="relative z-50 isolate"
  x-data="{
      uid: Math.random().toString(36).slice(2),
  // Set to true to show the flyout open by default. Change to false to have it closed by default.
  open: true,
      style: '',
      gap: 8,
      align() {
        const r = this.$refs.btn.getBoundingClientRect();
        const top = r.bottom + this.gap;              // full-width, just under button
        this.style = `position:fixed; left:0; right:0; top:${top}px;`;
      },
      toggle() {
        if (this.open) { this.open = false; return; }
        window.dispatchEvent(new CustomEvent('close-all-flyouts', { detail: this.uid }));
        this.align();                                  // compute coords first
        requestAnimationFrame(() => this.open = true); // then show (no jitter)
      },
      close() { this.open = false; }
    }"
  x-init="
      window.addEventListener('close-all-flyouts', (e) => { if (e.detail !== uid) open = false });
      const realign = () => { if (open) align() };
      window.addEventListener('resize', realign);
      window.addEventListener('scroll', realign, { passive: true });
      // If open by default, align immediately
      if (open) align();
    "
>
  <div
    class="flex items-center justify-center px-8 mx-auto max-w-7xl md:px-12 lg:px-24"
  >
    <button
      class="relative flex items-center justify-center text-center font-medium transition-colors duration-200 ease-in-out select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:z-10 justify-center rounded-md text-zinc-600 bg-zinc-50 outline outline-zinc-100 hover:bg-zinc-200 focus-visible:outline-zinc-600 h-9 px-4 text-sm gap-2.5"
      x-ref="btn"
      aria-expanded="false"
      @click.stop="toggle"
    >
      <span>Button</span>
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        class="icon icon-tabler-chevron-down size-4"
        slot="right-icon"
      >
        <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
        <path d="M6 9l6 6l6 -6"></path>
      </svg>
    </button>
  </div>
  <!-- Backdrop (outside click) -->
  <div
    x-show="open"
    x-transition.opacity
    class="fixed inset-0 z-[999]"
    @click="close"
    style="display: none"
  ></div>
  <!-- Teleported full-width flyout -->
  <template x-teleport="body">
    <div
      x-show="open"
      :style="style"
      class="z-[1000] bg-white shadow-lg outline outline-zinc-900/5"
      style="display: none"
      @keydown.escape.window="close"
      x-transition:enter="transition ease-out duration-200"
      x-transition:enter-start="opacity-0 -translate-y-1"
      x-transition:enter-end="opacity-100 translate-y-0"
      x-transition:leave="transition ease-in duration-150"
      x-transition:leave-start="opacity-100 translate-y-0"
      x-transition:leave-end="opacity-0 -translate-y-1"
    >
      <div class="px-8 py-8 mx-auto max-w-7xl md:px-12 lg:px-24">
        <div
          class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-x-8 gap-y-12"
        >
          <div>
            <h3 class="text-base font-medium text-zinc-900">
              Browse by category
            </h3>
            <div
              class="pt-4 mt-4 border-t border-zinc-200 space-y-2"
              role="none"
            >
              <a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                New Arrivals </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Best Sellers </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Seasonal Favorites </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Exclusive Collections </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Trending Styles </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Sale Items </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Limited Edition
              </a>
            </div>
          </div>
          <div>
            <h3 class="text-base font-medium text-zinc-900">Shop by feature</h3>
            <div
              class="pt-4 mt-4 border-t border-zinc-200 space-y-2"
              role="none"
            >
              <a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Free Shipping </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Sustainable Materials </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Custom Sizing </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Gift Cards </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Style Guides </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Personal Stylist </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Loyalty Program
              </a>
            </div>
          </div>
          <div>
            <h3 class="text-base font-medium text-zinc-900">
              Men&#39;s Collection
            </h3>
            <div
              class="pt-4 mt-4 border-t border-zinc-200 space-y-2"
              role="none"
            >
              <a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Casual Wear </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Formal Wear </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Sportswear </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Footwear </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Accessories </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Outerwear </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Activewear
              </a>
            </div>
          </div>
          <div>
            <h3 class="text-base font-medium text-zinc-900">
              Women&#39;s Collection
            </h3>
            <div
              class="pt-4 mt-4 border-t border-zinc-200 space-y-2"
              role="none"
            >
              <a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Dresses </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Tops &amp; Blouses </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Bottoms </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Shoes </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Jewelry &amp; Accessories </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Outerwear </a
              ><a
                class="text-base block font-medium hover:text-zinc-600 text-zinc-500"
                href="#"
              >
                Activewear
              </a>
            </div>
          </div>
        </div>
        <div
          class="items-end p-8 mt-12 shadow grid grid-cols-1 gap-4 sm:grid-cols-2 bg-linear-180 outline outline-zinc-100 from-zinc-50 to-zinc-100 rounded-xl"
        >
          <div>
            <h3 class="text-lg md:text-xl font-semibold text-zinc-900">
              Discover your perfect style
            </h3>
            <p class="text-base mt-4 font-medium text-zinc-500 lg:text-balance">
              Explore the latest trends and effortlessly shop exclusive
              collections, all while enjoying a personalized shopping experience
              tailored just for you
            </p>
          </div>
          <div class="flex flex-col items-center gap-2 lg:flex-row sm:ml-auto">
            <button
              class="relative flex items-center justify-center text-center font-medium transition-colors duration-200 ease-in-out select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:z-10 justify-center rounded-md text-white bg-zinc-900 outline outline-zinc-900 hover:bg-zinc-950 focus-visible:outline-zinc-950 h-9 px-4 text-sm"
            >
              Get started
            </button>
            <button
              class="relative flex items-center justify-center text-center font-medium transition-colors duration-200 ease-in-out select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:z-10 justify-center rounded-md text-zinc-600 bg-zinc-50 outline outline-zinc-100 hover:bg-zinc-200 focus-visible:outline-zinc-600 h-9 px-4 text-sm"
            >
              Learn more
            </button>
          </div>
        </div>
      </div>
    </div>
  </template>
</div>

Wrap‑up

You now have a flexible, full‑width mega menu that anchors to its trigger, teleports for correct stacking, and closes when users click outside or press Escape. For production, consider wiring :aria-expanded to the button, managing focus when opening/closing, and using prefers-reduced-motion to dampen transitions for motion‑sensitive users.

/Michael Andreuzza

Did you like this post? Please share it with your friends!