Tailwind Dropdown: 16 Examples of Menus and Pickers

Tailwind CSS ships no dropdown logic. Utilities style the menu, while native HTML, a little JavaScript or a headless library opens and closes it.

In v4, hover applies only on devices that can hover, so a group-hover menu stops opening on tap. Below are 16 Tailwind dropdown examples you can copy, followed by a guide to methods, accessibility and clipping fixes.

By Bogdan Sandu · Updated October 29, 2026

Every example is built with Tailwind CSS v4.3 utility classes. To try one quickly, add the Play CDN tags below to your page and paste the example's HTML, CSS and JS. The dark variant line makes dark: classes follow a .dark class instead of the system setting. For production, install Tailwind with Vite or the CLI.

<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
  @custom-variant dark (&:where(.dark, .dark *));
</style>

Native dropdowns with details and the Popover API

Dropdowns where the browser does the opening and closing: an exclusive details group for site navigation, and popover menus that get light dismiss, Escape and the top layer for free, placed with CSS anchor positioning where it is supported.

Exclusive details navigation

  • <details name>
  • group-open:
  • list-none
  • sr-only sm:not-sr-only

Click Guides, then Tools. Only one stays open.

Each menu is a details element, so Enter, Space and clicks toggle it with no script, and the shared name attribute makes the two menus exclusive: opening Tools closes Guides. group-open: styles the summary and rotates the chevron, which in v4 is the rotate property, so the transition lists rotate instead of transform. On phones the panels hang from the whole header (md:relative moves the containing block to the details element on wider screens), so they never run off the edge.

View code
<header class="relative w-full max-w-2xl rounded-2xl bg-white px-3 py-2 shadow-sm ring-1 ring-stone-900/5 md:px-4">
  <nav aria-label="Main" class="flex items-center gap-1">
    <a href="#" class="mr-auto flex min-h-11 items-center gap-2 rounded-lg pr-2 text-base font-bold tracking-tight text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700">
      <span aria-hidden="true" class="grid size-8 place-items-center rounded-lg bg-emerald-800 text-white"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" class="size-5"><path d="M12 21V9M12 13c-4 0-6-3-6-7 4 0 6 3 6 7zM12 11c3 0 5-2 5-5-3 0-5 2-5 5z"/></svg></span>
      <span class="sr-only sm:not-sr-only">Fernbook</span>
    </a>
    <details name="tdd-nav" class="group md:relative">
      <summary class="flex min-h-11 cursor-pointer list-none items-center gap-1 rounded-lg px-3 text-sm font-semibold text-stone-700 select-none hover:bg-stone-100 group-open:bg-emerald-50 group-open:text-emerald-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700 [&::-webkit-details-marker]:hidden">
        Guides
        <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" class="size-4 group-open:rotate-180 motion-safe:transition-[rotate]"><path d="m6 8 4 4 4-4"/></svg>
      </summary>
      <div class="absolute inset-x-2 top-full z-10 mt-1 rounded-xl bg-white p-2 shadow-lg ring-1 ring-stone-900/10 md:inset-x-auto md:left-0 md:mt-2 md:w-72">
        <a href="#" class="block rounded-lg px-3 py-2.5 hover:bg-stone-50 focus-visible:outline-2 focus-visible:outline-emerald-700">
          <span class="block text-sm font-semibold text-stone-900">Getting started</span>
          <span class="block text-sm text-stone-600">Plan and fill your first raised bed</span>
        </a>
        <a href="#" class="block rounded-lg px-3 py-2.5 hover:bg-stone-50 focus-visible:outline-2 focus-visible:outline-emerald-700">
          <span class="block text-sm font-semibold text-stone-900">Planting calendar</span>
          <span class="block text-sm text-stone-600">What to sow this month, by zone</span>
        </a>
        <a href="#" class="block rounded-lg px-3 py-2.5 hover:bg-stone-50 focus-visible:outline-2 focus-visible:outline-emerald-700">
          <span class="block text-sm font-semibold text-stone-900">Soil and compost</span>
          <span class="block text-sm text-stone-600">Feed the bed before the plants</span>
        </a>
      </div>
    </details>
    <details name="tdd-nav" class="group md:relative">
      <summary class="flex min-h-11 cursor-pointer list-none items-center gap-1 rounded-lg px-3 text-sm font-semibold text-stone-700 select-none hover:bg-stone-100 group-open:bg-emerald-50 group-open:text-emerald-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700 [&::-webkit-details-marker]:hidden">
        Tools
        <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" class="size-4 group-open:rotate-180 motion-safe:transition-[rotate]"><path d="m6 8 4 4 4-4"/></svg>
      </summary>
      <div class="absolute inset-x-2 top-full z-10 mt-1 rounded-xl bg-white p-2 shadow-lg ring-1 ring-stone-900/10 md:inset-x-auto md:right-0 md:mt-2 md:w-64">
        <a href="#" class="flex items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-sm font-semibold text-stone-900 hover:bg-stone-50 focus-visible:outline-2 focus-visible:outline-emerald-700">Garden planner <span class="rounded-full bg-emerald-50 px-2 py-0.5 text-xs font-semibold text-emerald-800">New</span></a>
        <a href="#" class="block rounded-lg px-3 py-2.5 text-sm font-semibold text-stone-900 hover:bg-stone-50 focus-visible:outline-2 focus-visible:outline-emerald-700">Frost date lookup</a>
        <a href="#" class="block rounded-lg px-3 py-2.5 text-sm font-semibold text-stone-900 hover:bg-stone-50 focus-visible:outline-2 focus-visible:outline-emerald-700">Seed spacing calculator</a>
      </div>
    </details>
    <a href="#" class="hidden min-h-11 items-center rounded-lg px-3 text-sm font-semibold text-stone-700 hover:bg-stone-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700 md:flex">Community</a>
  </nav>
</header>

Sort menu with the Popover API and anchor positioning

  • popovertarget
  • open:
  • starting:open:
  • transition-discrete
  • has-checked:

Click a trigger. Escape or a click outside closes a popover.

The panel is a popover="auto" element, so clicking outside or pressing Escape closes it and the browser reports the expanded state on the button. The choices are real radio buttons, so arrow keys move between them, and has-checked: highlights the row. open: and starting:open: give the panel an enter and exit fade inside motion-safe:, with transition-discrete keeping it visible until the fade ends. A short @supports block anchors it under the button with position-area; other browsers show it centered.

View code
<div class="tdd-sort w-full max-w-md rounded-2xl bg-white shadow-sm ring-1 ring-zinc-900/5">
  <div class="flex items-center justify-between gap-3 border-b border-zinc-100 px-4 py-3">
    <div>
      <p class="text-sm font-semibold text-zinc-900">Brand assets</p>
      <p class="text-sm text-zinc-500">4 files</p>
    </div>
    <button type="button" popovertarget="tdd-sort-pop" class="flex min-h-10 items-center gap-1.5 rounded-lg px-3 text-sm text-zinc-700 ring-1 ring-zinc-200 [anchor-name:--tdd-sort] hover:bg-zinc-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-700">
      <span class="text-zinc-500">Sort:</span>
      <span class="tdd-sort-label font-semibold text-zinc-900">Last modified</span>
      <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" class="size-4"><path d="m6 8 4 4 4-4"/></svg>
    </button>
  </div>
  <ul class="tdd-sort-list divide-y divide-zinc-100">
    <li data-name="Logo lockups.zip" data-modified="20261027" data-size="18400" class="flex items-center gap-3 px-4 py-3">
      <span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-amber-100 text-[11px] font-bold text-amber-900">ZIP</span>
      <span class="min-w-0 flex-1"><span class="block truncate text-sm font-semibold text-zinc-900">Logo lockups.zip</span><span class="block text-sm text-zinc-500">18.4 MB · Oct 27</span></span>
    </li>
    <li data-name="Social templates.fig" data-modified="20261025" data-size="41700" class="flex items-center gap-3 px-4 py-3">
      <span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-violet-100 text-[11px] font-bold text-violet-900">FIG</span>
      <span class="min-w-0 flex-1"><span class="block truncate text-sm font-semibold text-zinc-900">Social templates.fig</span><span class="block text-sm text-zinc-500">41.7 MB · Oct 25</span></span>
    </li>
    <li data-name="Color tokens.json" data-modified="20261021" data-size="12" class="flex items-center gap-3 px-4 py-3">
      <span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-sky-100 text-[11px] font-bold text-sky-900">JSON</span>
      <span class="min-w-0 flex-1"><span class="block truncate text-sm font-semibold text-zinc-900">Color tokens.json</span><span class="block text-sm text-zinc-500">12 KB · Oct 21</span></span>
    </li>
    <li data-name="Brand book.pdf" data-modified="20261014" data-size="6200" class="flex items-center gap-3 px-4 py-3">
      <span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-rose-100 text-[11px] font-bold text-rose-900">PDF</span>
      <span class="min-w-0 flex-1"><span class="block truncate text-sm font-semibold text-zinc-900">Brand book.pdf</span><span class="block text-sm text-zinc-500">6.2 MB · Oct 14</span></span>
    </li>
  </ul>
  <div id="tdd-sort-pop" popover class="tdd-sort-pos m-auto w-60 origin-top-right scale-95 rounded-xl bg-white p-1.5 opacity-0 shadow-lg ring-1 ring-zinc-900/10 [position-anchor:--tdd-sort] open:scale-100 open:opacity-100 motion-safe:transition-[opacity,scale,display,overlay] motion-safe:transition-discrete motion-safe:duration-150 motion-safe:ease-out motion-safe:starting:open:scale-95 motion-safe:starting:open:opacity-0">
    <fieldset>
      <legend class="px-3 pb-1 pt-2 text-sm font-semibold text-zinc-500">Sort by</legend>
      <label class="flex min-h-10 cursor-pointer items-center gap-3 rounded-lg px-3 text-sm text-zinc-700 hover:bg-zinc-50 has-checked:bg-violet-50 has-checked:font-semibold has-checked:text-violet-900 has-focus-visible:outline-2 has-focus-visible:outline-violet-700">
        <input type="radio" name="tdd-sort" value="modified" data-label="Last modified" class="peer sr-only" checked>
        <span class="flex-1">Last modified</span>
        <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="invisible size-4 text-violet-700 peer-checked:visible"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg>
      </label>
      <label class="flex min-h-10 cursor-pointer items-center gap-3 rounded-lg px-3 text-sm text-zinc-700 hover:bg-zinc-50 has-checked:bg-violet-50 has-checked:font-semibold has-checked:text-violet-900 has-focus-visible:outline-2 has-focus-visible:outline-violet-700">
        <input type="radio" name="tdd-sort" value="name" data-label="Name" class="peer sr-only">
        <span class="flex-1">Name</span>
        <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="invisible size-4 text-violet-700 peer-checked:visible"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg>
      </label>
      <label class="flex min-h-10 cursor-pointer items-center gap-3 rounded-lg px-3 text-sm text-zinc-700 hover:bg-zinc-50 has-checked:bg-violet-50 has-checked:font-semibold has-checked:text-violet-900 has-focus-visible:outline-2 has-focus-visible:outline-violet-700">
        <input type="radio" name="tdd-sort" value="size" data-label="File size" class="peer sr-only">
        <span class="flex-1">File size</span>
        <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="invisible size-4 text-violet-700 peer-checked:visible"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg>
      </label>
    </fieldset>
  </div>
</div>

Split button with a right-aligned popover

  • popover
  • position-area: bottom span-left
  • starting:open:
  • rounded-l-lg

Click a trigger. Escape or a click outside closes a popover.

The main action and the menu trigger are two buttons joined with rounded-l-lg and rounded-r-lg, and the chevron gets an aria-label because it has no text. Its popover anchors to the right edge with position-area: bottom span-left, so it grows leftward and stays inside the card. When an option is picked the script closes the popover, and the browser hands focus back to the trigger.

View code
<div class="tdd-split w-full max-w-md rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-900/5">
  <p class="text-base font-semibold text-slate-900">Spring lookbook: linen and clay</p>
  <p class="mt-1 text-sm text-slate-600">Journal post by Noor Hadid, 1,240 words, 9 images</p>
  <p class="tdd-split-status mt-3 rounded-lg bg-slate-50 px-3 py-2 text-sm text-slate-700" role="status">Draft saved 2 min ago</p>
  <div class="mt-5 flex items-center justify-end gap-2">
    <a href="#" class="flex min-h-11 items-center rounded-lg px-3 text-sm font-semibold text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-700">Preview</a>
    <div class="inline-flex rounded-lg shadow-xs">
      <button type="button" class="tdd-split-main min-h-11 rounded-l-lg bg-teal-700 px-4 text-sm font-semibold text-white hover:bg-teal-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-700">Publish</button>
      <button type="button" popovertarget="tdd-split-pop" aria-label="More publish options" class="grid min-h-11 w-11 place-items-center rounded-r-lg border-l border-teal-900/40 bg-teal-700 text-white [anchor-name:--tdd-split] hover:bg-teal-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-700">
        <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="size-4"><path d="m6 8 4 4 4-4"/></svg>
      </button>
    </div>
  </div>
  <div id="tdd-split-pop" popover class="tdd-split-pos m-auto w-64 origin-top-right rounded-xl bg-white p-1.5 shadow-lg ring-1 ring-slate-900/10 [position-anchor:--tdd-split] motion-safe:transition-[opacity,scale] motion-safe:duration-150 motion-safe:ease-out motion-safe:starting:open:scale-95 motion-safe:starting:open:opacity-0">
    <button type="button" data-msg="Scheduled for Monday, Nov 2 at 09:00" class="tdd-split-item flex w-full items-start gap-3 rounded-lg px-3 py-2.5 text-left hover:bg-slate-50 focus-visible:bg-slate-50 focus-visible:outline-2 focus-visible:outline-teal-700">
      <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="mt-0.5 size-5 shrink-0 text-slate-500"><path d="M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM12 7v5l3 2"/></svg>
      <span><span class="block text-sm font-semibold text-slate-900">Schedule</span><span class="block text-sm text-slate-600">Publish at a set date and time</span></span>
    </button>
    <button type="button" data-msg="Published and queued for Friday's newsletter" class="tdd-split-item flex w-full items-start gap-3 rounded-lg px-3 py-2.5 text-left hover:bg-slate-50 focus-visible:bg-slate-50 focus-visible:outline-2 focus-visible:outline-teal-700">
      <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="mt-0.5 size-5 shrink-0 text-slate-500"><path d="M3 6h18v12H3zM3 7l9 6 9-6"/></svg>
      <span><span class="block text-sm font-semibold text-slate-900">Publish and email</span><span class="block text-sm text-slate-600">Also send to 3,180 subscribers</span></span>
    </button>
    <button type="button" data-msg="Draft saved just now" class="tdd-split-item flex w-full items-start gap-3 rounded-lg px-3 py-2.5 text-left hover:bg-slate-50 focus-visible:bg-slate-50 focus-visible:outline-2 focus-visible:outline-teal-700">
      <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="mt-0.5 size-5 shrink-0 text-slate-500"><path d="M5 4h11l3 3v13H5zM8 4v5h7V4M8 20v-6h8v6"/></svg>
      <span><span class="block text-sm font-semibold text-slate-900">Save as draft</span><span class="block text-sm text-slate-600">Keep editing later</span></span>
    </button>
  </div>
</div>

Dropup attachment menu in a chat composer

  • position-area: top span-right
  • origin-bottom-left
  • focus-within:
  • popover

Click a trigger. Escape or a click outside closes a popover.

A composer sits at the bottom of its container, so its menu opens upward: position-area: top span-right places the popover above the plus button, and position-try-fallbacks flips it below if there is no room above. origin-bottom-left makes the starting:open: scale grow out of the button. The input row uses focus-within: for its ring, so the whole bar lights up when the field has focus.

View code
<div class="tdd-up flex w-full max-w-md flex-col rounded-2xl bg-white shadow-sm ring-1 ring-slate-900/5">
  <div class="flex items-center gap-3 border-b border-slate-100 px-4 py-3">
    <span aria-hidden="true" class="grid size-9 place-items-center rounded-lg bg-indigo-100 text-sm font-bold text-indigo-800">#</span>
    <div>
      <p class="text-sm font-semibold text-slate-900">launch-week</p>
      <p class="text-sm text-slate-500">8 members</p>
    </div>
  </div>
  <div class="space-y-4 px-4 py-4 text-sm">
    <div class="flex gap-3">
      <span aria-hidden="true" class="grid size-8 shrink-0 place-items-center rounded-full bg-emerald-700 text-xs font-semibold text-white">JT</span>
      <p class="text-slate-700"><span class="font-semibold text-slate-900">June Tran</span><br>Press kit is final. Can someone drop the hero shots in here?</p>
    </div>
    <div class="flex gap-3">
      <span aria-hidden="true" class="grid size-8 shrink-0 place-items-center rounded-full bg-rose-700 text-xs font-semibold text-white">OB</span>
      <p class="text-slate-700"><span class="font-semibold text-slate-900">Omar Bell</span><br>On it, and I will set up a poll for the launch time.</p>
    </div>
  </div>
  <div class="mt-auto border-t border-slate-100 p-3">
    <p class="tdd-up-chip mb-2 w-fit rounded-md bg-indigo-50 px-2 py-1 text-sm font-medium text-indigo-900" hidden></p>
    <div class="flex items-center gap-1.5 rounded-xl bg-slate-50 p-1.5 ring-1 ring-slate-200 focus-within:ring-2 focus-within:ring-indigo-600">
      <button type="button" popovertarget="tdd-up-pop" aria-label="Add attachment" class="grid size-10 shrink-0 place-items-center rounded-lg text-slate-600 [anchor-name:--tdd-up] hover:bg-white hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-indigo-600">
        <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" class="size-5"><path d="M12 5v14M5 12h14"/></svg>
      </button>
      <label for="tdd-up-msg" class="sr-only">Message launch-week</label>
      <input id="tdd-up-msg" type="text" placeholder="Message #launch-week" class="min-h-10 min-w-0 flex-1 bg-transparent text-sm text-slate-900 outline-none placeholder:text-slate-500">
      <button type="button" aria-label="Send message" class="grid size-10 shrink-0 place-items-center rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
        <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" class="size-5"><path d="M4 12 20 4l-6 16-2.5-6.5z"/></svg>
      </button>
    </div>
  </div>
  <div id="tdd-up-pop" popover class="tdd-up-pos m-auto w-56 origin-bottom-left rounded-xl bg-white p-1.5 shadow-lg ring-1 ring-slate-900/10 [position-anchor:--tdd-up] motion-safe:transition-[opacity,scale] motion-safe:duration-150 motion-safe:ease-out motion-safe:starting:open:scale-95 motion-safe:starting:open:opacity-0">
    <button type="button" data-name="hero-shots.zip" class="tdd-up-item flex min-h-10 w-full items-center gap-3 rounded-lg px-3 text-left text-sm text-slate-700 hover:bg-slate-50 focus-visible:bg-slate-50 focus-visible:outline-2 focus-visible:outline-indigo-600">
      <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5 text-slate-500"><path d="M12 16V4M7 9l5-5 5 5M5 20h14"/></svg>Upload a file
    </button>
    <button type="button" data-name="studio-photo.jpg" class="tdd-up-item flex min-h-10 w-full items-center gap-3 rounded-lg px-3 text-left text-sm text-slate-700 hover:bg-slate-50 focus-visible:bg-slate-50 focus-visible:outline-2 focus-visible:outline-indigo-600">
      <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5 text-slate-500"><path d="M4 5h16v14H4zM4 16l5-5 4 4 3-3 4 4M15 9.5h.01"/></svg>Photo from library
    </button>
    <button type="button" data-name="Poll: launch time" class="tdd-up-item flex min-h-10 w-full items-center gap-3 rounded-lg px-3 text-left text-sm text-slate-700 hover:bg-slate-50 focus-visible:bg-slate-50 focus-visible:outline-2 focus-visible:outline-indigo-600">
      <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5 text-slate-500"><path d="M5 20V10M12 20V4M19 20v-7"/></svg>Create a poll
    </button>
    <button type="button" data-name="Code snippet" class="tdd-up-item flex min-h-10 w-full items-center gap-3 rounded-lg px-3 text-left text-sm text-slate-700 hover:bg-slate-50 focus-visible:bg-slate-50 focus-visible:outline-2 focus-visible:outline-indigo-600">
      <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5 text-slate-500"><path d="m8 8-4 4 4 4M16 8l4 4-4 4"/></svg>Code snippet
    </button>
  </div>
</div>

Menu buttons with full keyboard support

Action menus that follow the W3C APG menu button pattern: aria-haspopup, aria-expanded, role="menu" and menuitem, arrow keys, Home and End, type-ahead, and Escape that returns focus to the button. Tailwind styles every state through aria-* variants.

File actions menu with icons and a destructive item

  • aria-haspopup="menu"
  • aria-expanded:
  • focus:bg-*
  • starting:
  • role="separator"

Click the button or focus it and press Enter, then use the arrow keys, Home, End and Escape.

A complete APG menu button: Enter, Space or Down Arrow opens the menu on the first item, Up Arrow on the last, arrow keys wrap, Home and End jump, a letter jumps to the matching item, and Escape closes and puts focus back on the button. Items use roving focus (tabindex="-1" plus focus()), so a single focus:bg-slate-100 class handles both keyboard and pointer highlight. The panel uses the hidden attribute, and motion-safe:starting:opacity-0 fades it in when hidden is removed. Delete gets its own red text and red focus color.

View code
<div class="tdd-act w-full max-w-md self-start rounded-2xl bg-white p-4 shadow-sm ring-1 ring-slate-900/5">
  <div class="flex items-center gap-3">
    <span aria-hidden="true" class="grid size-11 shrink-0 place-items-center rounded-lg bg-red-50 text-xs font-bold text-red-700">PDF</span>
    <div class="min-w-0 flex-1">
      <p class="truncate text-sm font-semibold text-slate-900">Brand-guidelines-v4.pdf</p>
      <p class="text-sm text-slate-500">2.4 MB · Edited 3 h ago</p>
    </div>
    <div class="relative">
      <button type="button" id="tdd-act-btn" aria-haspopup="menu" aria-expanded="false" aria-controls="tdd-act-menu" aria-label="Actions for Brand-guidelines-v4.pdf" class="tdd-act-btn grid size-11 place-items-center rounded-lg text-slate-600 hover:bg-slate-100 aria-expanded:bg-slate-100 aria-expanded:text-slate-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600">
        <svg aria-hidden="true" viewBox="0 0 20 20" fill="currentColor" class="size-5"><circle cx="10" cy="4" r="1.6"/><circle cx="10" cy="10" r="1.6"/><circle cx="10" cy="16" r="1.6"/></svg>
      </button>
      <div id="tdd-act-menu" role="menu" aria-labelledby="tdd-act-btn" hidden class="absolute right-0 top-full z-10 mt-2 w-56 origin-top-right rounded-xl bg-white p-1.5 shadow-lg ring-1 ring-slate-900/10 motion-safe:transition-[opacity,scale] motion-safe:duration-100 motion-safe:ease-out motion-safe:starting:scale-95 motion-safe:starting:opacity-0">
        <button type="button" role="menuitem" tabindex="-1" data-done="Opened in the viewer" class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-slate-700 focus:bg-slate-100 focus:text-slate-900 focus:outline-hidden">
          <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5 text-slate-500"><path d="M14 4h6v6M20 4l-9 9M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5"/></svg>Open
        </button>
        <button type="button" role="menuitem" tabindex="-1" data-done="Rename: type a new name" class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-slate-700 focus:bg-slate-100 focus:text-slate-900 focus:outline-hidden">
          <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5 text-slate-500"><path d="M4 20h4L18.5 9.5a2.1 2.1 0 0 0-3-3L5 17v3zM14 7l3 3"/></svg>Rename
        </button>
        <button type="button" role="menuitem" tabindex="-1" data-done="Copy created: Brand-guidelines-v4 (1).pdf" class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-slate-700 focus:bg-slate-100 focus:text-slate-900 focus:outline-hidden">
          <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5 text-slate-500"><path d="M8 8h11v12H8zM5 16V4h11"/></svg>Duplicate
        </button>
        <button type="button" role="menuitem" tabindex="-1" data-done="Share link copied for 7 days" class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-slate-700 focus:bg-slate-100 focus:text-slate-900 focus:outline-hidden">
          <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5 text-slate-500"><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/></svg>Share link
        </button>
        <div role="separator" class="mx-2 my-1.5 h-px bg-slate-100"></div>
        <button type="button" role="menuitem" tabindex="-1" data-done="Download started" class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-slate-700 focus:bg-slate-100 focus:text-slate-900 focus:outline-hidden">
          <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5 text-slate-500"><path d="M12 4v11M7 10l5 5 5-5M5 20h14"/></svg>Download
        </button>
        <div role="separator" class="mx-2 my-1.5 h-px bg-slate-100"></div>
        <button type="button" role="menuitem" tabindex="-1" data-done="Moved to trash. You can restore it for 30 days." class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm font-medium text-red-700 focus:bg-red-50 focus:text-red-800 focus:outline-hidden">
          <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5"><path d="M5 7h14M10 7V4h4v3M7 7l1 13h8l1-13M10 11v6M14 11v6"/></svg>Delete
        </button>
      </div>
    </div>
  </div>
  <p class="tdd-act-status mt-3 min-h-5 text-sm text-slate-600" role="status"></p>
</div>

Avatar account menu with sections

  • role="group"
  • menuitemradio
  • group-aria-checked:
  • divide-*

Click the button or focus it and press Enter, then use the arrow keys, Home, End and Escape.

The identity block sits above the role="menu" list instead of inside it, because a menu may only own items, groups and separators. The workspace switcher is a role="group" of menuitemradio items, and group-aria-checked:visible shows the check mark on the active one. Separators split profile links, workspaces and Sign out, and the panel is right-aligned with right-0 and origin-top-right so it grows out of the avatar.

View code
<div class="tdd-acc w-full max-w-xl self-start rounded-2xl bg-white shadow-sm ring-1 ring-stone-900/5">
  <div class="flex items-center justify-between gap-3 border-b border-stone-100 px-4 py-2">
    <a href="#" class="flex min-h-11 items-center gap-2 rounded-lg pr-2 text-sm font-bold text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-700">
      <span aria-hidden="true" class="grid size-8 place-items-center rounded-lg bg-stone-900 text-xs text-white">Ln</span>Lanternly
    </a>
    <div class="flex items-center gap-1">
      <a href="#" class="hidden min-h-11 items-center rounded-lg px-3 text-sm font-medium text-stone-600 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-700 sm:flex">Help</a>
      <div class="relative">
        <button type="button" id="tdd-acc-btn" aria-haspopup="menu" aria-expanded="false" aria-controls="tdd-acc-menu" aria-label="Account menu for Mara Okafor" class="tdd-acc-btn flex min-h-11 items-center gap-1.5 rounded-full p-1 pr-2 text-stone-500 hover:bg-stone-100 aria-expanded:bg-stone-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-700">
          <span aria-hidden="true" class="grid size-9 place-items-center rounded-full bg-orange-700 text-sm font-semibold text-white">MO</span>
          <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" class="size-4"><path d="m6 8 4 4 4-4"/></svg>
        </button>
        <div class="tdd-acc-panel absolute right-0 top-full z-10 mt-2 w-72 max-w-[calc(100vw-2.5rem)] origin-top-right rounded-xl bg-white shadow-lg ring-1 ring-stone-900/10 motion-safe:transition-[opacity,scale] motion-safe:duration-100 motion-safe:ease-out motion-safe:starting:scale-95 motion-safe:starting:opacity-0" hidden>
          <div class="flex items-center gap-3 border-b border-stone-100 px-4 py-3">
            <span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-full bg-orange-700 text-sm font-semibold text-white">MO</span>
            <div class="min-w-0 flex-1">
              <p class="truncate text-sm font-semibold text-stone-900">Mara Okafor</p>
              <p class="truncate text-sm text-stone-600">mara@example.com</p>
            </div>
            <span class="rounded-full bg-orange-50 px-2 py-0.5 text-xs font-semibold text-orange-800 ring-1 ring-orange-200">Pro</span>
          </div>
          <div id="tdd-acc-menu" role="menu" aria-label="Account" class="p-1.5">
            <button type="button" role="menuitem" tabindex="-1" data-done="Opening your profile" class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-sm text-stone-700 focus:bg-stone-100 focus:text-stone-900 focus:outline-hidden">
              <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5 text-stone-500"><path d="M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM4 21a8 8 0 0 1 16 0"/></svg>Your profile
            </button>
            <button type="button" role="menuitem" tabindex="-1" data-done="Opening settings" class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-sm text-stone-700 focus:bg-stone-100 focus:text-stone-900 focus:outline-hidden">
              <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5 text-stone-500"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM19 12h2M3 12h2M12 3v2M12 19v2M17 7l1.5-1.5M5.5 18.5 7 17M17 17l1.5 1.5M5.5 5.5 7 7"/></svg>Settings
            </button>
            <button type="button" role="menuitem" tabindex="-1" data-done="Opening billing" class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-sm text-stone-700 focus:bg-stone-100 focus:text-stone-900 focus:outline-hidden">
              <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5 text-stone-500"><path d="M3 6h18v12H3zM3 10h18M7 15h3"/></svg><span class="flex-1">Billing</span><span class="text-xs text-stone-500">Renews Nov 12</span>
            </button>
            <div role="separator" class="mx-2 my-1.5 h-px bg-stone-100"></div>
            <div role="group" aria-label="Workspace">
              <button type="button" role="menuitemradio" aria-checked="true" tabindex="-1" class="group flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-sm text-stone-700 focus:bg-stone-100 focus:text-stone-900 focus:outline-hidden">
                <span aria-hidden="true" class="grid size-5 place-items-center rounded bg-sky-800 text-[10px] font-bold text-white">H</span><span class="tdd-acc-name flex-1">Harbor Studio</span>
                <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="invisible size-4 text-orange-700 group-aria-checked:visible"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg>
              </button>
              <button type="button" role="menuitemradio" aria-checked="false" tabindex="-1" class="group flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-sm text-stone-700 focus:bg-stone-100 focus:text-stone-900 focus:outline-hidden">
                <span aria-hidden="true" class="grid size-5 place-items-center rounded bg-lime-800 text-[10px] font-bold text-white">K</span><span class="tdd-acc-name flex-1">Kettle and Co</span>
                <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="invisible size-4 text-orange-700 group-aria-checked:visible"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg>
              </button>
            </div>
            <div role="separator" class="mx-2 my-1.5 h-px bg-stone-100"></div>
            <button type="button" role="menuitem" tabindex="-1" data-done="Signed out of all Lanternly workspaces" class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-sm text-stone-700 focus:bg-stone-100 focus:text-stone-900 focus:outline-hidden">
              <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5 text-stone-500"><path d="M15 4h4v16h-4M10 16l4-4-4-4M14 12H4"/></svg>Sign out
            </button>
          </div>
        </div>
      </div>
    </div>
  </div>
  <div class="px-4 py-6">
    <p class="text-lg font-semibold text-stone-900">Good morning, Mara</p>
    <p class="tdd-acc-status mt-1 text-sm text-stone-600" role="status">Workspace: Harbor Studio</p>
  </div>
</div>

Inbox menu with a nested submenu

  • aria-haspopup
  • md:left-full
  • aria-expanded:bg-*
  • ArrowRight

Open Organize, move to Move to and press the Right Arrow, or hover it.

Move to is a menuitem that owns a second role="menu": Right Arrow, Enter or hovering opens it and focuses its first item, Left Arrow or Escape closes it and returns to Move to, and Escape again closes everything. On wide frames the submenu flies out to the side with md:left-full; on phones it overlaps the parent menu lower down, because there is no room beside it. aria-expanded:bg-slate-100 keeps the parent item highlighted while its submenu is open.

View code
<div class="tdd-nest w-full max-w-md rounded-2xl bg-white shadow-sm ring-1 ring-slate-900/5">
  <div class="flex items-center gap-3 border-b border-slate-100 px-4 py-3">
    <div class="relative">
      <button type="button" id="tdd-nest-btn" aria-haspopup="menu" aria-expanded="false" aria-controls="tdd-nest-menu" class="tdd-nest-btn flex min-h-10 items-center gap-1.5 rounded-lg bg-cyan-800 px-3 text-sm font-semibold text-white hover:bg-cyan-900 aria-expanded:bg-cyan-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-800">
        Organize
        <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" class="size-4"><path d="m6 8 4 4 4-4"/></svg>
      </button>
      <div id="tdd-nest-menu" role="menu" aria-labelledby="tdd-nest-btn" hidden class="absolute left-0 top-full z-10 mt-2 w-56 rounded-xl bg-white p-1.5 shadow-lg ring-1 ring-slate-900/10 motion-safe:transition-[opacity,translate] motion-safe:duration-100 motion-safe:ease-out motion-safe:starting:-translate-y-1 motion-safe:starting:opacity-0">
        <button type="button" role="menuitem" tabindex="-1" data-done="Archived 1 conversation" class="tdd-nest-item flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-slate-700 focus:bg-slate-100 focus:text-slate-900 focus:outline-hidden">
          <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5 text-slate-500"><path d="M4 5h16v4H4zM5 9v10h14V9M10 13h4"/></svg>Archive
        </button>
        <button type="button" role="menuitem" tabindex="-1" data-done="Marked as unread" class="tdd-nest-item flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-slate-700 focus:bg-slate-100 focus:text-slate-900 focus:outline-hidden">
          <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5 text-slate-500"><path d="M3 6h18v12H3zM3 7l9 6 9-6"/></svg>Mark as unread
        </button>
        <div role="none" class="relative">
          <button type="button" role="menuitem" tabindex="-1" aria-haspopup="menu" aria-expanded="false" aria-controls="tdd-nest-sub" class="tdd-nest-item tdd-nest-parent flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-slate-700 aria-expanded:bg-slate-100 focus:bg-slate-100 focus:text-slate-900 focus:outline-hidden">
            <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5 text-slate-500"><path d="M3 6h6l2 2h10v11H3z"/></svg><span class="flex-1">Move to</span>
            <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" class="size-4 text-slate-500"><path d="m8 6 4 4-4 4"/></svg>
          </button>
          <div id="tdd-nest-sub" role="menu" aria-label="Move to" hidden class="absolute left-10 top-full z-20 -mt-1 w-48 rounded-xl bg-white p-1.5 shadow-xl ring-1 ring-slate-900/10 md:left-full md:top-0 md:-mt-1.5 md:ml-2">
            <button type="button" role="menuitem" tabindex="-1" data-done="Moved to Clients" class="tdd-nest-subitem flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-slate-700 focus:bg-cyan-50 focus:text-cyan-900 focus:outline-hidden"><span aria-hidden="true" class="size-2 rounded-full bg-cyan-600"></span>Clients</button>
            <button type="button" role="menuitem" tabindex="-1" data-done="Moved to Receipts" class="tdd-nest-subitem flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-slate-700 focus:bg-cyan-50 focus:text-cyan-900 focus:outline-hidden"><span aria-hidden="true" class="size-2 rounded-full bg-amber-500"></span>Receipts</button>
            <button type="button" role="menuitem" tabindex="-1" data-done="Moved to Travel" class="tdd-nest-subitem flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-slate-700 focus:bg-cyan-50 focus:text-cyan-900 focus:outline-hidden"><span aria-hidden="true" class="size-2 rounded-full bg-violet-600"></span>Travel</button>
            <button type="button" role="menuitem" tabindex="-1" data-done="Moved to Family" class="tdd-nest-subitem flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-slate-700 focus:bg-cyan-50 focus:text-cyan-900 focus:outline-hidden"><span aria-hidden="true" class="size-2 rounded-full bg-rose-600"></span>Family</button>
          </div>
        </div>
        <button type="button" role="menuitem" tabindex="-1" data-done="Snoozed until tomorrow, 08:00" class="tdd-nest-item flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-slate-700 focus:bg-slate-100 focus:text-slate-900 focus:outline-hidden">
          <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5 text-slate-500"><path d="M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM12 7v5l3 2"/></svg>Snooze
        </button>
        <div role="separator" class="mx-2 my-1.5 h-px bg-slate-100"></div>
        <button type="button" role="menuitem" tabindex="-1" data-done="Reported as spam" class="tdd-nest-item flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-red-700 focus:bg-red-50 focus:text-red-800 focus:outline-hidden">
          <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5"><path d="M12 3l9 16H3zM12 10v4M12 17h.01"/></svg>Report spam
        </button>
      </div>
    </div>
    <p class="text-sm text-slate-500">1 selected</p>
  </div>
  <ul class="divide-y divide-slate-100 text-sm">
    <li class="flex items-start gap-3 bg-cyan-50/60 px-4 py-3">
      <span aria-hidden="true" class="mt-1 size-2 shrink-0 rounded-full bg-cyan-700"></span>
      <div class="min-w-0"><p class="font-semibold text-slate-900">Ines Moreau</p><p class="truncate text-slate-600">Signed proposal for the Alder Street fit-out</p></div>
    </li>
    <li class="flex items-start gap-3 px-4 py-3">
      <span aria-hidden="true" class="mt-1 size-2 shrink-0"></span>
      <div class="min-w-0"><p class="font-semibold text-slate-900">Northgate Rail</p><p class="truncate text-slate-600">Your e-ticket for Friday, 07:42 departure</p></div>
    </li>
  </ul>
  <p class="tdd-nest-status min-h-5 px-4 pb-4 text-sm text-slate-600" role="status"></p>
</div>

Dark mode editor menu with checkbox and radio items

  • dark:
  • menuitemcheckbox
  • menuitemradio
  • group-aria-checked:
  • <kbd>

Open View and switch the theme, or toggle the minimap and line numbers.

The wrapper carries the dark class, so every dark: utility inside follows it, and the Theme items flip that class live. Minimap and Line numbers are menuitemcheckbox items and Theme is a group of menuitemradio items; they keep the menu open when toggled, as the APG allows, and group-aria-checked:visible draws the check marks. Each item has a dark: pair for text, background and focus row, so the highlight never ends up as light text on a light row.

View code
<div class="tdd-view dark w-full max-w-lg">
  <div class="rounded-2xl bg-white text-zinc-900 shadow-sm ring-1 ring-zinc-900/5 dark:bg-zinc-900 dark:text-zinc-100 dark:ring-white/10">
    <div class="flex items-center gap-1 border-b border-zinc-200 px-2 py-1.5 dark:border-white/10">
      <span aria-hidden="true" class="flex gap-1.5 px-2"><span class="size-3 rounded-full bg-zinc-300 dark:bg-zinc-600"></span><span class="size-3 rounded-full bg-zinc-300 dark:bg-zinc-600"></span><span class="size-3 rounded-full bg-zinc-300 dark:bg-zinc-600"></span></span>
      <div class="relative">
        <button type="button" id="tdd-view-btn" aria-haspopup="menu" aria-expanded="false" aria-controls="tdd-view-menu" class="tdd-view-btn min-h-10 rounded-md px-3 text-sm font-medium text-zinc-700 hover:bg-zinc-100 aria-expanded:bg-zinc-100 focus-visible:outline-2 focus-visible:outline-lime-700 dark:text-zinc-300 dark:hover:bg-white/10 dark:aria-expanded:bg-white/10 dark:focus-visible:outline-lime-400">View</button>
        <div id="tdd-view-menu" role="menu" aria-labelledby="tdd-view-btn" hidden class="absolute left-0 top-full z-10 mt-1 w-64 rounded-xl bg-white p-1.5 shadow-lg ring-1 ring-zinc-900/10 motion-safe:transition-[opacity,scale] motion-safe:duration-100 motion-safe:ease-out motion-safe:starting:scale-95 motion-safe:starting:opacity-0 origin-top-left dark:bg-zinc-800 dark:shadow-black/40 dark:ring-white/10">
          <button type="button" role="menuitemcheckbox" aria-checked="true" tabindex="-1" data-toggle="map" class="group flex w-full items-center gap-3 rounded-lg px-2.5 py-2 text-left text-sm text-zinc-700 focus:bg-zinc-100 focus:text-zinc-900 focus:outline-hidden dark:text-zinc-200 dark:focus:bg-white/10 dark:focus:text-white">
            <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="invisible size-4 text-lime-700 group-aria-checked:visible dark:text-lime-400"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg>
            <span class="flex-1">Minimap</span><kbd class="font-sans text-xs text-zinc-500 dark:text-zinc-400">Ctrl M</kbd>
          </button>
          <button type="button" role="menuitemcheckbox" aria-checked="true" tabindex="-1" data-toggle="lines" class="group flex w-full items-center gap-3 rounded-lg px-2.5 py-2 text-left text-sm text-zinc-700 focus:bg-zinc-100 focus:text-zinc-900 focus:outline-hidden dark:text-zinc-200 dark:focus:bg-white/10 dark:focus:text-white">
            <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="invisible size-4 text-lime-700 group-aria-checked:visible dark:text-lime-400"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg>
            <span class="flex-1">Line numbers</span><kbd class="font-sans text-xs text-zinc-500 dark:text-zinc-400">Ctrl L</kbd>
          </button>
          <div role="separator" class="mx-2 my-1.5 h-px bg-zinc-100 dark:bg-white/10"></div>
          <div role="group" aria-label="Theme">
            <button type="button" role="menuitemradio" aria-checked="false" tabindex="-1" data-theme="light" class="group flex w-full items-center gap-3 rounded-lg px-2.5 py-2 text-left text-sm text-zinc-700 focus:bg-zinc-100 focus:text-zinc-900 focus:outline-hidden dark:text-zinc-200 dark:focus:bg-white/10 dark:focus:text-white">
              <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="invisible size-4 text-lime-700 group-aria-checked:visible dark:text-lime-400"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg>
              <span class="flex-1">Light theme</span>
            </button>
            <button type="button" role="menuitemradio" aria-checked="true" tabindex="-1" data-theme="dark" class="group flex w-full items-center gap-3 rounded-lg px-2.5 py-2 text-left text-sm text-zinc-700 focus:bg-zinc-100 focus:text-zinc-900 focus:outline-hidden dark:text-zinc-200 dark:focus:bg-white/10 dark:focus:text-white">
              <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="invisible size-4 text-lime-700 group-aria-checked:visible dark:text-lime-400"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg>
              <span class="flex-1">Dark theme</span>
            </button>
          </div>
        </div>
      </div>
      <p class="ml-auto truncate px-2 font-mono text-xs text-zinc-500 dark:text-zinc-400">tokens.css</p>
    </div>
    <div class="flex gap-3 p-4 font-mono text-xs leading-6">
      <div class="tdd-view-lines select-none text-right text-zinc-500 dark:text-zinc-500" aria-hidden="true">1<br>2<br>3<br>4<br>5<br>6</div>
      <pre class="min-w-0 flex-1 overflow-x-auto text-zinc-800 dark:text-zinc-200"><code><span class="text-fuchsia-700 dark:text-fuchsia-300">@theme</span> {
  <span class="text-sky-700 dark:text-sky-300">--color-moss-500</span>: #4d7c0f;
  <span class="text-sky-700 dark:text-sky-300">--color-moss-900</span>: #1a2e05;
  <span class="text-sky-700 dark:text-sky-300">--radius-card</span>: 14px;
  <span class="text-sky-700 dark:text-sky-300">--font-display</span>: "Fraunces";
}</code></pre>
      <div class="tdd-view-map hidden w-14 shrink-0 space-y-1.5 border-l border-zinc-200 pl-2 pt-1.5 md:block dark:border-white/10" aria-hidden="true">
        <span class="block h-1 w-8 rounded bg-fuchsia-300 dark:bg-fuchsia-400/60"></span>
        <span class="block h-1 w-11 rounded bg-sky-300 dark:bg-sky-400/50"></span>
        <span class="block h-1 w-10 rounded bg-sky-300 dark:bg-sky-400/50"></span>
        <span class="block h-1 w-7 rounded bg-sky-300 dark:bg-sky-400/50"></span>
        <span class="block h-1 w-9 rounded bg-sky-300 dark:bg-sky-400/50"></span>
        <span class="block h-1 w-2 rounded bg-zinc-300 dark:bg-zinc-600"></span>
      </div>
    </div>
  </div>
</div>

Search, filter and select dropdowns

Dropdowns that pick something: a searchable assignee picker, a multi-select status filter with a count badge, and a currency menu built from menuitemradio items and prefixed @theme colors.

Searchable assignee dropdown

  • aria-expanded
  • group-aria-pressed:
  • max-h-56 overflow-y-auto
  • role="status"

Open the Assignee field and type a name.

A disclosure, not a menu: the button opens a panel with a real search field and a list of buttons, so Tab and screen readers work without menu roles. Typing filters the list and a hidden live region reads the match count; Down Arrow moves from the field into the results, and Escape closes the panel and returns focus. aria-pressed marks the current assignee, which group-aria-pressed: turns into a check mark.

View code
<div class="tdd-as w-full max-w-sm self-start rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-900/5">
  <p class="text-base font-semibold text-slate-900">Checkout button jumps on Safari</p>
  <p class="mt-1 text-sm text-slate-500">WEB-482 · Opened by Lena Fischer</p>
  <dl class="mt-5 grid grid-cols-[5.5rem_1fr] items-center gap-y-3 text-sm">
    <dt id="tdd-as-label" class="text-slate-500">Assignee</dt>
    <dd class="relative">
      <button type="button" aria-expanded="false" aria-controls="tdd-as-panel" aria-labelledby="tdd-as-label tdd-as-value" class="tdd-as-btn flex min-h-10 w-full items-center gap-2 rounded-lg px-2 text-left text-slate-900 ring-1 ring-slate-200 hover:bg-slate-50 aria-expanded:ring-2 aria-expanded:ring-fuchsia-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fuchsia-700">
        <span aria-hidden="true" class="tdd-as-avatar grid size-6 shrink-0 place-items-center rounded-full bg-slate-200 text-[10px] font-semibold text-slate-700">?</span>
        <span id="tdd-as-value" class="flex-1 truncate">Unassigned</span>
        <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" class="size-4 text-slate-500"><path d="m6 8 4 4 4-4"/></svg>
      </button>
      <div id="tdd-as-panel" hidden class="absolute right-0 top-full z-10 mt-2 w-64 rounded-xl bg-white p-2 shadow-lg ring-1 ring-slate-900/10 motion-safe:transition-[opacity,translate] motion-safe:duration-100 motion-safe:ease-out motion-safe:starting:-translate-y-1 motion-safe:starting:opacity-0">
        <label for="tdd-as-q" class="sr-only">Search people</label>
        <div class="relative">
          <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-slate-500"><path d="M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14zM20 20l-4-4"/></svg>
          <input id="tdd-as-q" type="search" autocomplete="off" placeholder="Search people" aria-describedby="tdd-as-count" class="tdd-as-q min-h-10 w-full rounded-lg bg-slate-50 pl-9 pr-3 text-sm text-slate-900 ring-1 ring-slate-200 placeholder:text-slate-500 focus:outline-2 focus:outline-fuchsia-700">
        </div>
        <ul class="tdd-as-list mt-2 max-h-48 overflow-y-auto">
          <li><button type="button" aria-pressed="false" data-initials="AL" class="tdd-as-opt group flex min-h-10 w-full items-center gap-2.5 rounded-lg px-2 text-left text-sm text-slate-700 hover:bg-slate-50 focus:bg-fuchsia-50 focus:text-fuchsia-900 focus:outline-hidden aria-pressed:font-semibold"><span aria-hidden="true" class="grid size-6 place-items-center rounded-full bg-sky-700 text-[10px] font-semibold text-white">AL</span><span class="tdd-as-name flex-1">Ada Lindqvist</span><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="invisible size-4 text-fuchsia-700 group-aria-pressed:visible"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg></button></li>
          <li><button type="button" aria-pressed="false" data-initials="BC" class="tdd-as-opt group flex min-h-10 w-full items-center gap-2.5 rounded-lg px-2 text-left text-sm text-slate-700 hover:bg-slate-50 focus:bg-fuchsia-50 focus:text-fuchsia-900 focus:outline-hidden aria-pressed:font-semibold"><span aria-hidden="true" class="grid size-6 place-items-center rounded-full bg-emerald-700 text-[10px] font-semibold text-white">BC</span><span class="tdd-as-name flex-1">Bruno Castell</span><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="invisible size-4 text-fuchsia-700 group-aria-pressed:visible"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg></button></li>
          <li><button type="button" aria-pressed="false" data-initials="CW" class="tdd-as-opt group flex min-h-10 w-full items-center gap-2.5 rounded-lg px-2 text-left text-sm text-slate-700 hover:bg-slate-50 focus:bg-fuchsia-50 focus:text-fuchsia-900 focus:outline-hidden aria-pressed:font-semibold"><span aria-hidden="true" class="grid size-6 place-items-center rounded-full bg-amber-700 text-[10px] font-semibold text-white">CW</span><span class="tdd-as-name flex-1">Chen Wei</span><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="invisible size-4 text-fuchsia-700 group-aria-pressed:visible"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg></button></li>
          <li><button type="button" aria-pressed="false" data-initials="DO" class="tdd-as-opt group flex min-h-10 w-full items-center gap-2.5 rounded-lg px-2 text-left text-sm text-slate-700 hover:bg-slate-50 focus:bg-fuchsia-50 focus:text-fuchsia-900 focus:outline-hidden aria-pressed:font-semibold"><span aria-hidden="true" class="grid size-6 place-items-center rounded-full bg-rose-700 text-[10px] font-semibold text-white">DO</span><span class="tdd-as-name flex-1">Dana Okoye</span><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="invisible size-4 text-fuchsia-700 group-aria-pressed:visible"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg></button></li>
          <li><button type="button" aria-pressed="false" data-initials="EM" class="tdd-as-opt group flex min-h-10 w-full items-center gap-2.5 rounded-lg px-2 text-left text-sm text-slate-700 hover:bg-slate-50 focus:bg-fuchsia-50 focus:text-fuchsia-900 focus:outline-hidden aria-pressed:font-semibold"><span aria-hidden="true" class="grid size-6 place-items-center rounded-full bg-violet-700 text-[10px] font-semibold text-white">EM</span><span class="tdd-as-name flex-1">Eliza Marsh</span><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="invisible size-4 text-fuchsia-700 group-aria-pressed:visible"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg></button></li>
          <li><button type="button" aria-pressed="false" data-initials="FH" class="tdd-as-opt group flex min-h-10 w-full items-center gap-2.5 rounded-lg px-2 text-left text-sm text-slate-700 hover:bg-slate-50 focus:bg-fuchsia-50 focus:text-fuchsia-900 focus:outline-hidden aria-pressed:font-semibold"><span aria-hidden="true" class="grid size-6 place-items-center rounded-full bg-teal-700 text-[10px] font-semibold text-white">FH</span><span class="tdd-as-name flex-1">Farid Haddad</span><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="invisible size-4 text-fuchsia-700 group-aria-pressed:visible"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg></button></li>
        </ul>
        <p class="tdd-as-empty px-2 py-3 text-sm text-slate-500" hidden>No one matches that name.</p>
        <p id="tdd-as-count" class="tdd-as-count sr-only" role="status"></p>
      </div>
    </dd>
    <dt class="text-slate-500">Status</dt>
    <dd><span class="rounded-full bg-amber-50 px-2 py-0.5 text-xs font-semibold text-amber-800 ring-1 ring-amber-200">In review</span></dd>
    <dt class="text-slate-500">Due</dt>
    <dd class="text-slate-900">Friday, Nov 6</dd>
    <dt class="text-slate-500">Labels</dt>
    <dd class="flex flex-wrap gap-1.5"><span class="rounded-md bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-700">checkout</span><span class="rounded-md bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-700">safari</span></dd>
  </dl>
  <p class="mt-5 border-t border-slate-100 pt-4 text-sm leading-6 text-slate-600">On Safari 18 the Pay now button shifts 4px down when the promo code field gets focus, but only while the sticky summary is open.</p>
</div>

Multi-select checkbox filter with a count badge

  • accent-teal-700
  • popovertargetaction="hide"
  • hidden
  • has-checked:

Open Status and tick or untick a few boxes.

Native checkboxes inside a popover keep Space, Tab and form semantics, and accent-teal-700 colors them with no custom styling. The script counts the checked boxes, writes the number into the badge (hidden when zero, with a screen reader version inside the button) and filters the order list live. The Done button closes the popover with popovertargetaction="hide", no script needed.

View code
<div class="tdd-ms w-full max-w-lg rounded-2xl bg-white shadow-sm ring-1 ring-slate-900/5">
  <div class="flex items-center gap-2 border-b border-slate-100 px-4 py-3">
    <div class="mr-auto">
      <p class="text-sm font-semibold text-slate-900">Recent orders</p>
      <p class="tdd-ms-summary text-sm text-slate-500">Showing 3 of 5</p>
    </div>
    <button type="button" popovertarget="tdd-ms-pop" class="flex min-h-10 items-center gap-2 rounded-lg px-3 text-sm font-medium text-slate-700 ring-1 ring-slate-200 [anchor-name:--tdd-ms] hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-700">
      <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-4 text-slate-500"><path d="M4 5h16l-6 7v6l-4 2v-8z"/></svg>
      Status
      <span class="tdd-ms-badge grid min-w-5 place-items-center rounded-full bg-teal-700 px-1.5 text-xs font-semibold text-white" aria-hidden="true">2</span>
      <span class="tdd-ms-sr sr-only">, 2 selected</span>
    </button>
  </div>
  <ul class="tdd-ms-rows divide-y divide-slate-100 text-sm">
    <li data-status="paid" class="flex items-center gap-3 px-4 py-3"><span class="w-14 font-mono text-slate-500">#4821</span><span class="min-w-0 flex-1 truncate text-slate-900">Ines Ward</span><span class="hidden text-slate-700 sm:block">$128.00</span><span class="rounded-full bg-emerald-50 px-2 py-0.5 text-xs font-semibold text-emerald-800">Paid</span></li>
    <li data-status="pending" class="flex items-center gap-3 px-4 py-3"><span class="w-14 font-mono text-slate-500">#4820</span><span class="min-w-0 flex-1 truncate text-slate-900">Tomas Reyes</span><span class="hidden text-slate-700 sm:block">$54.20</span><span class="rounded-full bg-amber-50 px-2 py-0.5 text-xs font-semibold text-amber-800">Pending</span></li>
    <li data-status="paid" class="flex items-center gap-3 px-4 py-3"><span class="w-14 font-mono text-slate-500">#4819</span><span class="min-w-0 flex-1 truncate text-slate-900">Hana Sato</span><span class="hidden text-slate-700 sm:block">$310.00</span><span class="rounded-full bg-emerald-50 px-2 py-0.5 text-xs font-semibold text-emerald-800">Paid</span></li>
    <li data-status="shipped" hidden class="flex items-center gap-3 px-4 py-3"><span class="w-14 font-mono text-slate-500">#4818</span><span class="min-w-0 flex-1 truncate text-slate-900">Mia Kowalski</span><span class="hidden text-slate-700 sm:block">$76.90</span><span class="rounded-full bg-sky-50 px-2 py-0.5 text-xs font-semibold text-sky-800">Shipped</span></li>
    <li data-status="refunded" hidden class="flex items-center gap-3 px-4 py-3"><span class="w-14 font-mono text-slate-500">#4817</span><span class="min-w-0 flex-1 truncate text-slate-900">Owen Price</span><span class="hidden text-slate-700 sm:block">$42.00</span><span class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-700">Refunded</span></li>
  </ul>
  <div id="tdd-ms-pop" popover class="tdd-ms-pos m-auto w-64 rounded-xl bg-white shadow-lg ring-1 ring-slate-900/10 [position-anchor:--tdd-ms] motion-safe:transition-[opacity,translate] motion-safe:duration-150 motion-safe:ease-out motion-safe:starting:open:-translate-y-1 motion-safe:starting:open:opacity-0">
    <fieldset class="p-1.5">
      <legend class="sr-only">Filter orders by status</legend>
      <label class="flex min-h-10 cursor-pointer items-center gap-3 rounded-lg px-2.5 text-sm text-slate-600 hover:bg-slate-50 has-checked:font-medium has-checked:text-slate-900"><input type="checkbox" value="paid" checked class="tdd-ms-box size-4 accent-teal-700"><span aria-hidden="true" class="size-2 rounded-full bg-emerald-500"></span><span class="flex-1">Paid</span><span class="text-xs text-slate-500">2</span></label>
      <label class="flex min-h-10 cursor-pointer items-center gap-3 rounded-lg px-2.5 text-sm text-slate-600 hover:bg-slate-50 has-checked:font-medium has-checked:text-slate-900"><input type="checkbox" value="pending" checked class="tdd-ms-box size-4 accent-teal-700"><span aria-hidden="true" class="size-2 rounded-full bg-amber-500"></span><span class="flex-1">Pending</span><span class="text-xs text-slate-500">1</span></label>
      <label class="flex min-h-10 cursor-pointer items-center gap-3 rounded-lg px-2.5 text-sm text-slate-600 hover:bg-slate-50 has-checked:font-medium has-checked:text-slate-900"><input type="checkbox" value="shipped" class="tdd-ms-box size-4 accent-teal-700"><span aria-hidden="true" class="size-2 rounded-full bg-sky-500"></span><span class="flex-1">Shipped</span><span class="text-xs text-slate-500">1</span></label>
      <label class="flex min-h-10 cursor-pointer items-center gap-3 rounded-lg px-2.5 text-sm text-slate-600 hover:bg-slate-50 has-checked:font-medium has-checked:text-slate-900"><input type="checkbox" value="refunded" class="tdd-ms-box size-4 accent-teal-700"><span aria-hidden="true" class="size-2 rounded-full bg-slate-400"></span><span class="flex-1">Refunded</span><span class="text-xs text-slate-500">1</span></label>
    </fieldset>
    <div class="flex items-center justify-between border-t border-slate-100 px-2 py-2">
      <button type="button" class="tdd-ms-clear min-h-9 rounded-md px-2.5 text-sm font-medium text-slate-600 hover:bg-slate-50 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-700">Clear</button>
      <button type="button" popovertarget="tdd-ms-pop" popovertargetaction="hide" class="min-h-9 rounded-md bg-teal-700 px-3 text-sm font-semibold text-white hover:bg-teal-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-700">Done</button>
    </div>
  </div>
</div>

Currency menu with @theme colors

  • @theme
  • bg-tdd-ink
  • menuitemradio
  • Intl.NumberFormat

Open the currency menu and pick one, or press a letter to jump.

Two prefixed @theme tokens, --color-tdd-ink and --color-tdd-sand, become real utilities (bg-tdd-ink, focus:bg-tdd-sand, text-tdd-ink) that work with every variant. The menu is a set of menuitemradio items with aria-checked and type-ahead, so pressing E jumps to Euro. Picking one converts the prices with Intl.NumberFormat using fixed sample rates.

View code
<div class="tdd-cur w-full max-w-sm overflow-visible rounded-2xl bg-white shadow-sm ring-1 ring-slate-900/5">
  <div class="flex items-center justify-between gap-3 rounded-t-2xl bg-tdd-ink px-5 py-3">
    <p class="text-sm font-semibold text-white">Order summary</p>
    <div class="relative">
      <button type="button" id="tdd-cur-btn" aria-haspopup="menu" aria-expanded="false" aria-controls="tdd-cur-menu" class="tdd-cur-btn flex min-h-10 items-center gap-1.5 rounded-lg bg-white/10 px-3 text-sm font-semibold text-white hover:bg-white/20 aria-expanded:bg-white/20 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white">
        <span class="sr-only">Currency:</span><span class="tdd-cur-code">USD</span>
        <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" class="size-4"><path d="m6 8 4 4 4-4"/></svg>
      </button>
      <div id="tdd-cur-menu" role="menu" aria-labelledby="tdd-cur-btn" hidden class="absolute right-0 top-full z-10 mt-2 w-60 origin-top-right rounded-xl bg-white p-1.5 shadow-lg ring-1 ring-tdd-ink/15 motion-safe:transition-[opacity,scale] motion-safe:duration-100 motion-safe:ease-out motion-safe:starting:scale-95 motion-safe:starting:opacity-0">
        <button type="button" role="menuitemradio" aria-checked="true" tabindex="-1" data-code="USD" data-rate="1" class="group flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-slate-700 focus:bg-tdd-sand focus:text-tdd-ink focus:outline-hidden aria-checked:font-semibold"><span class="w-9 font-mono text-xs text-slate-500">USD</span><span class="flex-1">US dollar</span><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="invisible size-4 text-tdd-ink group-aria-checked:visible"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg></button>
        <button type="button" role="menuitemradio" aria-checked="false" tabindex="-1" data-code="EUR" data-rate="0.92" class="group flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-slate-700 focus:bg-tdd-sand focus:text-tdd-ink focus:outline-hidden aria-checked:font-semibold"><span class="w-9 font-mono text-xs text-slate-500">EUR</span><span class="flex-1">Euro</span><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="invisible size-4 text-tdd-ink group-aria-checked:visible"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg></button>
        <button type="button" role="menuitemradio" aria-checked="false" tabindex="-1" data-code="GBP" data-rate="0.78" class="group flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-slate-700 focus:bg-tdd-sand focus:text-tdd-ink focus:outline-hidden aria-checked:font-semibold"><span class="w-9 font-mono text-xs text-slate-500">GBP</span><span class="flex-1">British pound</span><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="invisible size-4 text-tdd-ink group-aria-checked:visible"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg></button>
        <button type="button" role="menuitemradio" aria-checked="false" tabindex="-1" data-code="JPY" data-rate="150" class="group flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-slate-700 focus:bg-tdd-sand focus:text-tdd-ink focus:outline-hidden aria-checked:font-semibold"><span class="w-9 font-mono text-xs text-slate-500">JPY</span><span class="flex-1">Japanese yen</span><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="invisible size-4 text-tdd-ink group-aria-checked:visible"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg></button>
        <button type="button" role="menuitemradio" aria-checked="false" tabindex="-1" data-code="CAD" data-rate="1.38" class="group flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-slate-700 focus:bg-tdd-sand focus:text-tdd-ink focus:outline-hidden aria-checked:font-semibold"><span class="w-9 font-mono text-xs text-slate-500">CAD</span><span class="flex-1">Canadian dollar</span><svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" class="invisible size-4 text-tdd-ink group-aria-checked:visible"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg></button>
      </div>
    </div>
  </div>
  <dl class="space-y-3 px-5 py-4 text-sm">
    <div class="flex justify-between gap-4"><dt class="text-slate-600">Linen throw, oat</dt><dd class="tdd-cur-price font-medium text-slate-900" data-usd="89">$89.00</dd></div>
    <div class="flex justify-between gap-4"><dt class="text-slate-600">Stoneware mug set</dt><dd class="tdd-cur-price font-medium text-slate-900" data-usd="46">$46.00</dd></div>
    <div class="flex justify-between gap-4"><dt class="text-slate-600">Shipping</dt><dd class="tdd-cur-price font-medium text-slate-900" data-usd="8">$8.00</dd></div>
    <div class="flex justify-between gap-4 border-t border-slate-100 pt-3 text-base"><dt class="font-semibold text-slate-900">Total</dt><dd class="tdd-cur-price font-bold text-tdd-ink" data-usd="143">$143.00</dd></div>
  </dl>
  <p class="rounded-b-2xl bg-tdd-sand px-5 py-3 text-xs text-slate-700">Sample rates for the demo. You are charged in US dollars.</p>
</div>

Mega panels, placement and hover

Bigger and trickier panels: a mega dropdown, a right-aligned panel with the classic 100 ms in, 75 ms out timing, row menus that flip upward near the bottom, a hover menu that also works by click and keyboard, and a notification panel on a dark theme.

Mega dropdown panel with a featured story

  • md:absolute md:inset-x-0
  • grid-cols-[1fr_1fr_15rem]
  • group-aria-expanded:
  • starting:

Click Products.

The Products button is a disclosure (aria-expanded and aria-controls), because the panel holds a page of links rather than commands. On wide frames md:absolute md:inset-x-0 hangs the panel across the full header width with a three-column grid-cols-[1fr_1fr_15rem] layout; on phones it stays in the flow and pushes the page down, which is how mobile mega menus usually behave. Escape and a click outside close it, and the chevron turns with group-aria-expanded:rotate-180.

View code
<div class="tdd-mega relative bg-white">
  <header class="relative border-b border-slate-200 bg-white">
    <nav aria-label="Main" class="mx-auto flex max-w-5xl items-center gap-1 px-4 py-2 md:px-6">
      <a href="#" class="mr-3 flex min-h-11 items-center gap-2 rounded-lg pr-1 text-base font-bold tracking-tight text-slate-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-700">
        <svg aria-hidden="true" viewBox="0 0 24 24" class="size-7 text-blue-700" fill="currentColor"><path d="M4 4h10l6 6v10H4z" opacity=".25"/><path d="M4 4h10v6h6v10H4z"/></svg>Quarry
      </a>
      <button type="button" aria-expanded="false" aria-controls="tdd-mega-panel" class="tdd-mega-btn group flex min-h-11 items-center gap-1 rounded-lg px-3 text-sm font-semibold text-slate-700 hover:bg-slate-100 aria-expanded:bg-blue-50 aria-expanded:text-blue-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-700">
        Products
        <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" class="size-4 group-aria-expanded:rotate-180 motion-safe:transition-[rotate]"><path d="m6 8 4 4 4-4"/></svg>
      </button>
      <a href="#" class="hidden min-h-11 items-center rounded-lg px-3 text-sm font-semibold text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-700 md:flex">Customers</a>
      <a href="#" class="hidden min-h-11 items-center rounded-lg px-3 text-sm font-semibold text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-700 md:flex">Pricing</a>
      <a href="#" class="ml-auto flex min-h-11 items-center rounded-lg bg-blue-700 px-4 text-sm font-semibold text-white hover:bg-blue-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-700">Start free</a>
    </nav>
    <div id="tdd-mega-panel" hidden class="border-t border-slate-200 bg-white md:absolute md:inset-x-0 md:top-full md:z-10 md:shadow-xl md:shadow-slate-900/10 motion-safe:transition-[opacity,translate] motion-safe:duration-150 motion-safe:ease-out motion-safe:starting:-translate-y-1 motion-safe:starting:opacity-0">
      <div class="mx-auto grid max-w-5xl gap-4 px-4 py-5 md:grid-cols-[1fr_1fr_15rem] md:gap-6 md:px-6">
        <div>
          <p id="tdd-mega-build" class="px-3 text-sm font-semibold text-slate-500">Build</p>
          <ul aria-labelledby="tdd-mega-build" class="mt-1 space-y-1">
            <li><a href="#" class="flex gap-3 rounded-lg p-3 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-blue-700"><span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-blue-50 text-blue-700"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5"><path d="M4 5h16v14H4zM4 9h16M9 9v10"/></svg></span><span><span class="block text-sm font-semibold text-slate-900">Site builder</span><span class="block text-sm text-slate-600">Drag in sections, publish in minutes</span></span></a></li>
            <li><a href="#" class="flex gap-3 rounded-lg p-3 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-blue-700"><span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-blue-50 text-blue-700"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5"><path d="M5 4h14v16H5zM8 8h8M8 12h8M8 16h5"/></svg></span><span><span class="block text-sm font-semibold text-slate-900">Forms</span><span class="block text-sm text-slate-600">Bookings, quotes and sign-ups</span></span></a></li>
            <li><a href="#" class="flex gap-3 rounded-lg p-3 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-blue-700"><span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-blue-50 text-blue-700"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5"><path d="M6 8h12l-1 12H7zM9 8V6a3 3 0 0 1 6 0v2"/></svg></span><span><span class="block text-sm font-semibold text-slate-900">Commerce</span><span class="block text-sm text-slate-600">Sell products and gift cards</span></span></a></li>
          </ul>
        </div>
        <div>
          <p id="tdd-mega-grow" class="px-3 text-sm font-semibold text-slate-500">Grow</p>
          <ul aria-labelledby="tdd-mega-grow" class="mt-1 space-y-1">
            <li><a href="#" class="flex gap-3 rounded-lg p-3 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-blue-700"><span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-orange-50 text-orange-700"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5"><path d="M5 20v-9M12 20V5M19 20v-6M3 20h18"/></svg></span><span><span class="block text-sm font-semibold text-slate-900">Analytics</span><span class="block text-sm text-slate-600">Cookie-free visitor stats</span></span></a></li>
            <li><a href="#" class="flex gap-3 rounded-lg p-3 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-blue-700"><span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-orange-50 text-orange-700"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5"><path d="M3 6h18v12H3zM3 7l9 6 9-6"/></svg></span><span><span class="block text-sm font-semibold text-slate-900">Email campaigns</span><span class="block text-sm text-slate-600">Newsletters that match your site</span></span></a></li>
            <li><a href="#" class="flex gap-3 rounded-lg p-3 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-blue-700"><span aria-hidden="true" class="grid size-10 shrink-0 place-items-center rounded-lg bg-orange-50 text-orange-700"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-5"><path d="M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14zM20 20l-4-4"/></svg></span><span><span class="block text-sm font-semibold text-slate-900">Search tools</span><span class="block text-sm text-slate-600">Titles, sitemaps and redirects</span></span></a></li>
          </ul>
        </div>
        <a href="#" class="block overflow-hidden rounded-xl bg-slate-50 ring-1 ring-slate-200 hover:ring-slate-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-700">
          <img src="city-skyline.webp" width="800" height="600" alt="Aerial view of a lakeside city at sunset" class="aspect-[16/9] w-full object-cover md:aspect-[4/3]">
          <span class="block p-3">
            <span class="block text-xs font-semibold text-blue-800">Customer story</span>
            <span class="mt-1 block text-sm font-semibold text-slate-900">How Lakeshore Rentals doubled direct bookings in one season</span>
          </span>
        </a>
      </div>
      <div class="border-t border-slate-100 bg-slate-50">
        <div class="mx-auto flex max-w-5xl flex-wrap gap-x-6 gap-y-1 px-4 py-2 text-sm md:px-6">
          <a href="#" class="flex min-h-10 items-center font-semibold text-slate-700 hover:text-blue-800 focus-visible:outline-2 focus-visible:outline-blue-700">Watch the 3-minute tour</a>
          <a href="#" class="flex min-h-10 items-center font-semibold text-slate-700 hover:text-blue-800 focus-visible:outline-2 focus-visible:outline-blue-700">Compare plans</a>
          <a href="#" class="flex min-h-10 items-center font-semibold text-slate-700 hover:text-blue-800 focus-visible:outline-2 focus-visible:outline-blue-700">Talk to sales</a>
        </div>
      </div>
    </div>
  </header>
  <div class="mx-auto max-w-5xl px-4 py-12 md:px-6 md:py-16">
    <p class="max-w-xl text-3xl font-bold tracking-tight text-slate-900 md:text-4xl">Websites for small teams that move fast</p>
    <p class="mt-3 max-w-lg text-base text-slate-600">Pages, forms, a shop and a newsletter in one place, without a plugin to update.</p>
  </div>
</div>

Right-aligned export menu with 100 ms in, 75 ms out

  • peer-aria-expanded:
  • right-0 origin-top-right
  • duration-75
  • transition-[opacity,scale,visibility]

Open each panel with a click, or hover where the demo says so.

The classic absolutely positioned dropdown: right-0 pins the panel to the button edge and origin-top-right makes the scale grow from that corner. The button is the peer, so peer-aria-expanded: drives the open state from the aria-expanded value the script sets, with a 100 ms ease-out enter and a 75 ms ease-in leave. visibility is only in the closing transition, so the panel is focusable the moment it opens and fully hidden once it closes.

View code
<div class="tdd-ex w-full max-w-lg rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-900/5">
  <div class="flex items-start justify-between gap-3">
    <div>
      <p class="text-sm font-medium text-slate-600">Revenue, last 30 days</p>
      <p class="mt-1 text-3xl font-bold tracking-tight text-slate-900">$48,920</p>
      <p class="mt-1 text-sm font-medium text-emerald-700">+12.4% on September</p>
    </div>
    <div class="relative">
      <button type="button" aria-expanded="false" aria-controls="tdd-ex-panel" class="tdd-ex-btn peer flex min-h-10 items-center gap-1.5 rounded-lg px-3 text-sm font-semibold text-slate-700 ring-1 ring-slate-200 hover:bg-slate-50 aria-expanded:bg-slate-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700">
        <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-4 text-slate-500"><path d="M12 4v11M7 10l5 5 5-5M5 20h14"/></svg>
        Export
      </button>
      <div id="tdd-ex-panel" class="invisible absolute right-0 top-full z-10 mt-2 w-60 origin-top-right scale-95 rounded-xl bg-white p-1.5 opacity-0 shadow-lg ring-1 ring-slate-900/10 motion-safe:transition-[opacity,scale,visibility] motion-safe:duration-75 motion-safe:ease-in peer-aria-expanded:visible peer-aria-expanded:scale-100 peer-aria-expanded:opacity-100 peer-aria-expanded:motion-safe:transition-[opacity,scale] peer-aria-expanded:motion-safe:duration-100 peer-aria-expanded:motion-safe:ease-out">
        <a href="#" class="tdd-ex-link flex items-center justify-between rounded-lg px-3 py-2 text-sm text-slate-700 hover:bg-emerald-50 hover:text-emerald-900 focus-visible:bg-emerald-50 focus-visible:outline-2 focus-visible:outline-emerald-700">CSV spreadsheet <span class="font-mono text-xs text-slate-500">.csv</span></a>
        <a href="#" class="tdd-ex-link flex items-center justify-between rounded-lg px-3 py-2 text-sm text-slate-700 hover:bg-emerald-50 hover:text-emerald-900 focus-visible:bg-emerald-50 focus-visible:outline-2 focus-visible:outline-emerald-700">PDF report <span class="font-mono text-xs text-slate-500">.pdf</span></a>
        <a href="#" class="tdd-ex-link flex items-center justify-between rounded-lg px-3 py-2 text-sm text-slate-700 hover:bg-emerald-50 hover:text-emerald-900 focus-visible:bg-emerald-50 focus-visible:outline-2 focus-visible:outline-emerald-700">Chart image <span class="font-mono text-xs text-slate-500">.png</span></a>
        <div class="mx-2 my-1.5 h-px bg-slate-100"></div>
        <a href="#" class="tdd-ex-link block rounded-lg px-3 py-2 text-sm text-slate-700 hover:bg-emerald-50 hover:text-emerald-900 focus-visible:bg-emerald-50 focus-visible:outline-2 focus-visible:outline-emerald-700">Email this report weekly</a>
      </div>
    </div>
  </div>
  <div class="mt-6 flex h-28 items-end gap-1 md:gap-1.5" aria-hidden="true">
    <span class="h-[38%] flex-1 rounded-t bg-emerald-200"></span><span class="h-[44%] flex-1 rounded-t bg-emerald-200"></span><span class="h-[41%] flex-1 rounded-t bg-emerald-200"></span><span class="h-[52%] flex-1 rounded-t bg-emerald-200"></span><span class="h-[48%] flex-1 rounded-t bg-emerald-200"></span><span class="h-[57%] flex-1 rounded-t bg-emerald-200"></span><span class="h-[61%] flex-1 rounded-t bg-emerald-200"></span><span class="h-[55%] flex-1 rounded-t bg-emerald-200"></span><span class="h-[66%] flex-1 rounded-t bg-emerald-200"></span><span class="h-[72%] flex-1 rounded-t bg-emerald-200"></span><span class="h-[69%] flex-1 rounded-t bg-emerald-200"></span><span class="h-[78%] flex-1 rounded-t bg-emerald-200"></span><span class="h-[84%] flex-1 rounded-t bg-emerald-200"></span><span class="h-[80%] flex-1 rounded-t bg-emerald-200"></span><span class="h-[92%] flex-1 rounded-t bg-emerald-600"></span>
  </div>
  <p class="sr-only">Daily revenue climbed through the month and peaked on the last day.</p>
  <div class="mt-2 flex justify-between text-xs text-slate-500"><span>Oct 1</span><span>Oct 30</span></div>
</div>

Row menus that flip into a dropup

  • position-try-fallbacks: flip-block
  • [anchor-name:--*]
  • popover
  • toggle event

Open the menu on the first row, then on the last row.

Every row opens its own popover anchored to its kebab button with position-area: bottom span-left. When the menu would run past the bottom of the viewport, as it does on the last rows, position-try-fallbacks: flip-block turns it into a dropup automatically. In browsers without anchor positioning, the script listens for the popover toggle event and places the menu with getBoundingClientRect instead.

View code
<div class="tdd-rk w-full max-w-lg rounded-2xl bg-white shadow-sm ring-1 ring-slate-900/5">
  <div class="flex items-center justify-between px-5 pt-4">
    <p class="text-sm font-semibold text-slate-900">Team members</p>
    <p class="tdd-rk-status text-sm text-slate-500" role="status">4 people</p>
  </div>
  <ul class="mt-2 divide-y divide-slate-100">
    <li class="flex items-center gap-3 px-5 py-2.5">
      <span aria-hidden="true" class="grid size-9 shrink-0 place-items-center rounded-full bg-purple-700 text-xs font-semibold text-white">RS</span>
      <div class="min-w-0 flex-1"><p class="truncate text-sm font-semibold text-slate-900">Rosa Salgado</p><p class="truncate text-sm text-slate-500">Owner</p></div>
      <button type="button" popovertarget="tdd-rk-1" aria-label="Actions for Rosa Salgado" class="grid size-10 shrink-0 place-items-center rounded-lg text-slate-600 [anchor-name:--tdd-rk-1] hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-purple-700"><svg aria-hidden="true" viewBox="0 0 20 20" fill="currentColor" class="size-5"><circle cx="10" cy="4" r="1.6"/><circle cx="10" cy="10" r="1.6"/><circle cx="10" cy="16" r="1.6"/></svg></button>
      <div id="tdd-rk-1" popover data-name="Rosa Salgado" class="tdd-rk-pop m-auto w-52 rounded-xl bg-white p-1.5 shadow-lg ring-1 ring-slate-900/10 [position-anchor:--tdd-rk-1]">
        <button type="button" data-msg="Role picker for" class="tdd-rk-item flex min-h-10 w-full items-center rounded-lg px-3 text-left text-sm text-slate-700 hover:bg-slate-50 focus-visible:bg-slate-50 focus-visible:outline-2 focus-visible:outline-purple-700">Change role</button>
        <button type="button" data-msg="Invite sent again to" class="tdd-rk-item flex min-h-10 w-full items-center rounded-lg px-3 text-left text-sm text-slate-700 hover:bg-slate-50 focus-visible:bg-slate-50 focus-visible:outline-2 focus-visible:outline-purple-700">Resend invite</button>
        <button type="button" data-msg="Removal pending for" class="tdd-rk-item flex min-h-10 w-full items-center rounded-lg px-3 text-left text-sm font-medium text-red-700 hover:bg-red-50 focus-visible:bg-red-50 focus-visible:outline-2 focus-visible:outline-red-700">Remove from team</button>
      </div>
    </li>
    <li class="flex items-center gap-3 px-5 py-2.5">
      <span aria-hidden="true" class="grid size-9 shrink-0 place-items-center rounded-full bg-sky-700 text-xs font-semibold text-white">TB</span>
      <div class="min-w-0 flex-1"><p class="truncate text-sm font-semibold text-slate-900">Theo Brandt</p><p class="truncate text-sm text-slate-500">Admin</p></div>
      <button type="button" popovertarget="tdd-rk-2" aria-label="Actions for Theo Brandt" class="grid size-10 shrink-0 place-items-center rounded-lg text-slate-600 [anchor-name:--tdd-rk-2] hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-purple-700"><svg aria-hidden="true" viewBox="0 0 20 20" fill="currentColor" class="size-5"><circle cx="10" cy="4" r="1.6"/><circle cx="10" cy="10" r="1.6"/><circle cx="10" cy="16" r="1.6"/></svg></button>
      <div id="tdd-rk-2" popover data-name="Theo Brandt" class="tdd-rk-pop m-auto w-52 rounded-xl bg-white p-1.5 shadow-lg ring-1 ring-slate-900/10 [position-anchor:--tdd-rk-2]">
        <button type="button" data-msg="Role picker for" class="tdd-rk-item flex min-h-10 w-full items-center rounded-lg px-3 text-left text-sm text-slate-700 hover:bg-slate-50 focus-visible:bg-slate-50 focus-visible:outline-2 focus-visible:outline-purple-700">Change role</button>
        <button type="button" data-msg="Invite sent again to" class="tdd-rk-item flex min-h-10 w-full items-center rounded-lg px-3 text-left text-sm text-slate-700 hover:bg-slate-50 focus-visible:bg-slate-50 focus-visible:outline-2 focus-visible:outline-purple-700">Resend invite</button>
        <button type="button" data-msg="Removal pending for" class="tdd-rk-item flex min-h-10 w-full items-center rounded-lg px-3 text-left text-sm font-medium text-red-700 hover:bg-red-50 focus-visible:bg-red-50 focus-visible:outline-2 focus-visible:outline-red-700">Remove from team</button>
      </div>
    </li>
    <li class="flex items-center gap-3 px-5 py-2.5">
      <span aria-hidden="true" class="grid size-9 shrink-0 place-items-center rounded-full bg-emerald-700 text-xs font-semibold text-white">PR</span>
      <div class="min-w-0 flex-1"><p class="truncate text-sm font-semibold text-slate-900">Priya Raman</p><p class="truncate text-sm text-slate-500">Editor</p></div>
      <button type="button" popovertarget="tdd-rk-3" aria-label="Actions for Priya Raman" class="grid size-10 shrink-0 place-items-center rounded-lg text-slate-600 [anchor-name:--tdd-rk-3] hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-purple-700"><svg aria-hidden="true" viewBox="0 0 20 20" fill="currentColor" class="size-5"><circle cx="10" cy="4" r="1.6"/><circle cx="10" cy="10" r="1.6"/><circle cx="10" cy="16" r="1.6"/></svg></button>
      <div id="tdd-rk-3" popover data-name="Priya Raman" class="tdd-rk-pop m-auto w-52 rounded-xl bg-white p-1.5 shadow-lg ring-1 ring-slate-900/10 [position-anchor:--tdd-rk-3]">
        <button type="button" data-msg="Role picker for" class="tdd-rk-item flex min-h-10 w-full items-center rounded-lg px-3 text-left text-sm text-slate-700 hover:bg-slate-50 focus-visible:bg-slate-50 focus-visible:outline-2 focus-visible:outline-purple-700">Change role</button>
        <button type="button" data-msg="Invite sent again to" class="tdd-rk-item flex min-h-10 w-full items-center rounded-lg px-3 text-left text-sm text-slate-700 hover:bg-slate-50 focus-visible:bg-slate-50 focus-visible:outline-2 focus-visible:outline-purple-700">Resend invite</button>
        <button type="button" data-msg="Removal pending for" class="tdd-rk-item flex min-h-10 w-full items-center rounded-lg px-3 text-left text-sm font-medium text-red-700 hover:bg-red-50 focus-visible:bg-red-50 focus-visible:outline-2 focus-visible:outline-red-700">Remove from team</button>
      </div>
    </li>
    <li class="flex items-center gap-3 px-5 py-2.5">
      <span aria-hidden="true" class="grid size-9 shrink-0 place-items-center rounded-full bg-slate-200 text-xs font-semibold text-slate-700">KM</span>
      <div class="min-w-0 flex-1"><p class="truncate text-sm font-semibold text-slate-900">Kofi Mensah</p><p class="truncate text-sm text-amber-800">Invited, not joined yet</p></div>
      <button type="button" popovertarget="tdd-rk-4" aria-label="Actions for Kofi Mensah" class="grid size-10 shrink-0 place-items-center rounded-lg text-slate-600 [anchor-name:--tdd-rk-4] hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-purple-700"><svg aria-hidden="true" viewBox="0 0 20 20" fill="currentColor" class="size-5"><circle cx="10" cy="4" r="1.6"/><circle cx="10" cy="10" r="1.6"/><circle cx="10" cy="16" r="1.6"/></svg></button>
      <div id="tdd-rk-4" popover data-name="Kofi Mensah" class="tdd-rk-pop m-auto w-52 rounded-xl bg-white p-1.5 shadow-lg ring-1 ring-slate-900/10 [position-anchor:--tdd-rk-4]">
        <button type="button" data-msg="Role picker for" class="tdd-rk-item flex min-h-10 w-full items-center rounded-lg px-3 text-left text-sm text-slate-700 hover:bg-slate-50 focus-visible:bg-slate-50 focus-visible:outline-2 focus-visible:outline-purple-700">Change role</button>
        <button type="button" data-msg="Invite sent again to" class="tdd-rk-item flex min-h-10 w-full items-center rounded-lg px-3 text-left text-sm text-slate-700 hover:bg-slate-50 focus-visible:bg-slate-50 focus-visible:outline-2 focus-visible:outline-purple-700">Resend invite</button>
        <button type="button" data-msg="Removal pending for" class="tdd-rk-item flex min-h-10 w-full items-center rounded-lg px-3 text-left text-sm font-medium text-red-700 hover:bg-red-50 focus-visible:bg-red-50 focus-visible:outline-2 focus-visible:outline-red-700">Remove from team</button>
      </div>
    </li>
  </ul>
</div>

Hover menu that also opens on click and keyboard

  • group-data-open:
  • pt-2 bridge
  • (hover: hover)
  • Escape (WCAG 1.4.13)

Hover Platform with a mouse, or click it. Press Escape to dismiss.

Hover is an extra here, not the only way in: the button toggles the panel on click and keyboard, and hover opening is only wired up when matchMedia reports a fine pointer that can hover, the same condition the v4 hover: variant compiles to. The panel has pt-2 padding instead of a margin, so there is no dead gap between button and panel, and a 250 ms close delay forgives a pointer that slips off. Escape closes it even while the pointer stays on it, as WCAG 1.4.13 asks.

View code
<div class="tdd-hv relative w-full max-w-2xl self-start rounded-2xl bg-sky-950 px-3 py-2 shadow-sm md:px-4">
  <nav aria-label="Docs" class="flex items-center gap-1">
    <a href="#" class="mr-auto flex min-h-11 items-center gap-2 rounded-lg pr-2 text-base font-bold tracking-tight text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-300">
      <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" class="size-6 text-sky-300"><path d="M3 16c3 0 3-2 6-2s3 2 6 2 3-2 6-2M3 11c3 0 3-2 6-2s3 2 6 2 3-2 6-2"/></svg>
      <span class="sr-only sm:not-sr-only">Driftwood Docs</span>
    </a>
    <div class="tdd-hv-item group md:relative">
      <button type="button" aria-expanded="false" aria-controls="tdd-hv-panel" class="tdd-hv-btn flex min-h-11 items-center gap-1 rounded-lg px-3 text-sm font-semibold text-sky-100 hover:bg-white/10 aria-expanded:bg-white/10 aria-expanded:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-300">
        Platform
        <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" class="size-4 group-data-open:rotate-180 motion-safe:transition-[rotate]"><path d="m6 8 4 4 4-4"/></svg>
      </button>
      <div id="tdd-hv-panel" class="invisible absolute inset-x-2 top-full z-10 pt-2 opacity-0 group-data-open:visible group-data-open:opacity-100 motion-safe:transition-[opacity,visibility] motion-safe:duration-150 md:inset-x-auto md:left-0 md:w-80">
        <div class="rounded-xl bg-white p-2 shadow-xl ring-1 ring-black/5">
          <a href="#" class="block rounded-lg px-3 py-2.5 hover:bg-sky-50 focus-visible:outline-2 focus-visible:outline-sky-700"><span class="block text-sm font-semibold text-slate-900">API reference</span><span class="block text-sm text-slate-600">Every endpoint, with request samples</span></a>
          <a href="#" class="block rounded-lg px-3 py-2.5 hover:bg-sky-50 focus-visible:outline-2 focus-visible:outline-sky-700"><span class="block text-sm font-semibold text-slate-900">Webhooks</span><span class="block text-sm text-slate-600">Events, retries and signatures</span></a>
          <a href="#" class="block rounded-lg px-3 py-2.5 hover:bg-sky-50 focus-visible:outline-2 focus-visible:outline-sky-700"><span class="block text-sm font-semibold text-slate-900">SDKs</span><span class="block text-sm text-slate-600">JavaScript, Python, Go and Ruby</span></a>
          <a href="#" class="flex items-center justify-between rounded-lg px-3 py-2.5 hover:bg-sky-50 focus-visible:outline-2 focus-visible:outline-sky-700"><span class="text-sm font-semibold text-slate-900">Status and uptime</span><span class="flex items-center gap-1.5 text-xs font-medium text-emerald-800"><span aria-hidden="true" class="size-2 rounded-full bg-emerald-500"></span>All systems normal</span></a>
        </div>
      </div>
    </div>
    <a href="#" class="flex min-h-11 items-center rounded-lg px-3 text-sm font-semibold text-sky-100 hover:bg-white/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-300">Guides</a>
    <a href="#" class="hidden min-h-11 items-center rounded-lg px-3 text-sm font-semibold text-sky-100 hover:bg-white/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-300 md:flex">Changelog</a>
  </nav>
</div>

Notification panel on a dark theme

  • dark:
  • data-unread:
  • group-data-unread:
  • popover

Click the bell, then Mark all as read.

This stage puts the dark class on the html element, so the panel uses its dark: utilities, and the same markup works on a light page. Unread rows carry a data-unread attribute: data-unread:bg-sky-50 (dark:data-unread:bg-white/5) tints the row and group-data-unread: lights the amber dot. Mark all as read removes the attributes, hides the badge and updates the button label, and the popover keeps light dismiss and Escape.

View code
<div class="tdd-nt w-full max-w-md self-start">
  <div class="flex items-center justify-between rounded-2xl bg-slate-900 px-4 py-2 ring-1 ring-white/10">
    <p class="text-sm font-semibold text-white">Ridgeline Ops</p>
    <button type="button" popovertarget="tdd-nt-pop" aria-label="Notifications, 3 unread" class="tdd-nt-btn relative grid size-11 place-items-center rounded-full text-slate-300 [anchor-name:--tdd-nt] hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-300">
      <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-6"><path d="M6 16v-5a6 6 0 1 1 12 0v5l1.5 2h-15zM10 20a2 2 0 0 0 4 0"/></svg>
      <span class="tdd-nt-badge absolute right-1 top-1 grid min-w-4 place-items-center rounded-full bg-amber-400 px-1 text-[10px] font-bold leading-4 text-slate-950" aria-hidden="true">3</span>
    </button>
  </div>
  <div id="tdd-nt-pop" popover class="tdd-nt-pos m-auto w-80 max-w-[calc(100vw-2rem)] overflow-hidden rounded-2xl bg-white text-slate-900 shadow-2xl ring-1 ring-slate-900/10 dark:bg-slate-900 dark:text-slate-100 dark:ring-white/10 motion-safe:transition-[opacity,translate] motion-safe:duration-150 motion-safe:ease-out motion-safe:starting:open:-translate-y-1 motion-safe:starting:open:opacity-0">
    <div class="flex items-center justify-between border-b border-slate-100 px-4 py-2 dark:border-white/10">
      <p id="tdd-nt-title" class="text-sm font-semibold">Notifications</p>
      <button type="button" class="tdd-nt-read min-h-9 rounded-md px-2 text-sm font-medium text-sky-700 hover:bg-sky-50 disabled:text-slate-500 disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-sky-600 dark:text-sky-300 dark:hover:bg-white/5 dark:disabled:text-slate-400">Mark all as read</button>
    </div>
    <ul aria-labelledby="tdd-nt-title" class="divide-y divide-slate-100 dark:divide-white/5">
      <li data-unread class="tdd-nt-row group flex gap-3 px-4 py-3 data-unread:bg-sky-50 dark:data-unread:bg-white/5">
        <span aria-hidden="true" class="mt-1.5 size-2 shrink-0 rounded-full group-data-unread:bg-amber-400"></span>
        <div class="min-w-0 text-sm"><p><span class="tdd-nt-sr sr-only">Unread: </span><span class="font-semibold">Deploy failed</span> on api-gateway, build 1287</p><p class="mt-0.5 text-xs text-slate-500 dark:text-slate-400">4 min ago</p></div>
      </li>
      <li data-unread class="tdd-nt-row group flex gap-3 px-4 py-3 data-unread:bg-sky-50 dark:data-unread:bg-white/5">
        <span aria-hidden="true" class="mt-1.5 size-2 shrink-0 rounded-full group-data-unread:bg-amber-400"></span>
        <div class="min-w-0 text-sm"><p><span class="tdd-nt-sr sr-only">Unread: </span><span class="font-semibold">Yara Nilsen</span> asked you to review the incident notes</p><p class="mt-0.5 text-xs text-slate-500 dark:text-slate-400">22 min ago</p></div>
      </li>
      <li data-unread class="tdd-nt-row group flex gap-3 px-4 py-3 data-unread:bg-sky-50 dark:data-unread:bg-white/5">
        <span aria-hidden="true" class="mt-1.5 size-2 shrink-0 rounded-full group-data-unread:bg-amber-400"></span>
        <div class="min-w-0 text-sm"><p><span class="tdd-nt-sr sr-only">Unread: </span>Disk usage on <span class="font-semibold">db-replica-2</span> passed 85%</p><p class="mt-0.5 text-xs text-slate-500 dark:text-slate-400">1 h ago</p></div>
      </li>
      <li class="tdd-nt-row group flex gap-3 px-4 py-3 data-unread:bg-sky-50 dark:data-unread:bg-white/5">
        <span aria-hidden="true" class="mt-1.5 size-2 shrink-0 rounded-full group-data-unread:bg-amber-400"></span>
        <div class="min-w-0 text-sm"><p>Weekly uptime report: <span class="font-semibold">99.98%</span></p><p class="mt-0.5 text-xs text-slate-500 dark:text-slate-400">Monday</p></div>
      </li>
    </ul>
    <a href="#" class="block border-t border-slate-100 px-4 py-3 text-center text-sm font-semibold text-sky-700 hover:bg-slate-50 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600 dark:border-white/10 dark:text-sky-300 dark:hover:bg-white/5">View all activity</a>
  </div>
</div>

Does Tailwind CSS have a built-in dropdown?

Not in the sense most people mean. The framework generates utility classes and state variants, and none of that includes open or close logic.

A dropdown on a Tailwind page is styled markup with behavior borrowed from elsewhere. That follows from how the framework is designed, since it produces classes and leaves interaction to the page.

The borrowing happens in a few ways. Native HTML offers details and summary, or the popover attribute. CSS state variants like group-focus-within, group-hover and peer-focus can fake a toggle. Alpine.js flips an open flag with a small script, and headless libraries (Headless UI, or the custom elements from Tailwind Plus) take over the whole job.

Tailwind Labs sells official dropdown markup in Tailwind Plus (formerly Tailwind UI). Before July 25, 2025, anyone outside React and Vue had to write the open and close script by hand.

That day the team shipped @tailwindplus/elements, a set of custom elements such as el-dropdown and el-menu that load from a single script tag. It is available exclusively to Tailwind Plus customers (Tailwind Labs, 2025).

Under the hood they all share the anatomy of a dropdown as a UI pattern. There’s a trigger and a panel with items inside it, and sometimes dividers or section headers.

Which Tailwind dropdown method fits the project?

Pick the lightest method whose failure conditions the page never meets. A single settings menu usually needs only plain HTML, while a site menu used with keyboards and screen readers needs a headless library.

MethodStrengthFails when
Plain HTML and CSS (details, focus, popover)No script, no dependencyOutside-click close or Escape is required
Alpine.jsClick, outside click and Escape in a few attributesArrow keys and focus return are left to you
Headless UIFull keyboard support and ARIA wiringThe project is Vue, where examples follow the older v1 API
Flowbite, Preline, daisyUIReady-made styled dropdownsThe design departs from the library defaults
Tailwind Plus ElementsFramework-free, keyboard and ARIA built inNo Tailwind Plus license

Pure CSS techniques for dropdown menus predate utility frameworks, and the same state tricks carry over to Tailwind variants.

Flowbite is an open-source component library with a dropdown component built in. Preline takes the plugin route, a JavaScript dropdown that adds an hs-dropdown-open: variant so you can style the open state. daisyUI uses component classes and documents methods built on details and summary or on CSS focus, plus a popover API version with anchor positioning. It also has a dropdown-hover modifier.

daisyUI’s documentation says the popover method renders the menu in the browser top layer, so z-index management and overflow clipping stop mattering. Positioning then depends on CSS anchor positioning, which reached Baseline Newly available in January 2026, so the remaining constraint is any visitor on a browser older than Chrome 125, Safari 26 or Firefox 147.

For one-off menus, details and summary beat any library. The extra dependency buys nothing until outside-click close or arrow-key movement becomes a requirement.

How to build a dropdown with HTML and utility classes only

A details element wraps everything, with a summary as the trigger and an absolutely positioned panel for the menu. No script involved.

  1. Wrap the trigger and panel in a positioned parent. Give the details element relative, inline-block and group, so the panel positions against it and children can react to its state.
  2. Style the summary like a button. Add list-none to hide the default marker, then padding, rounded and ring classes. A chevron with group-open:rotate-180 shows the open state.
  3. Position the panel with absolute, right-0 (or left-0), z-10, mt-2 and a fixed width such as w-56. Add origin-top-right now if you plan to animate later.
  4. Give the panel a surface. bg-white, rounded-md, shadow-lg and ring-1 ring-black/5 is the combination Tailwind Plus uses in its own dropdown markup.
  5. Style the items. Each link gets block, px-4, py-2, text-sm and focus:bg-gray-100, so keyboard focus is visible.
  6. When menus sit side by side, give the details elements the same name attribute and only one stays open at a time (MDN, details element reference).

Since details and summary are built into HTML itself, the browser toggles the panel on Enter and Space without any script.

The approach has limits, and they show up fast.

  • There’s no outside-click close. According to daisyUI’s docs, the panel opens and closes only when the summary is clicked.
  • Escape does nothing.
  • Utility classes alone can’t animate it. MDN says the ::details-content pseudo-element is what makes the open and close transition animatable, and it takes a little custom CSS.
  • The focus-based variant (group-focus-within) closes when focus leaves, so a dialog placed inside the panel kills it.

Should a Tailwind dropdown open on hover or on click?

Click. Hover only works as an addition that also responds to click and keyboard focus.

Usability write-ups from Liquid Light and Codelight get there from different directions. Liquid Light argues that hover menus leave little room for error, and Codelight points out that a hover-only parent item hides whether it is a link or just a label.

Tailwind galleries often lean the other way, with plenty of hover-opened examples, many of them written for Tailwind CSS v3.

Click is the safer call when touch users are in the audience, when the menu holds actions rather than links, or when keyboard users have to reach the panel at all. Hover is fine on a desktop-only menu if stakeholders insist, as long as click and focus open the same panel and the parent item isn’t a link itself.

Tailwind v4 changed the footing. The hover variant now compiles inside a media query for devices whose primary input supports hover, so a v3 menu built on hidden group-hover:block stops opening on tap (Tailwind CSS upgrade guide, as of the v4.3 docs).

The same guide recommends treating hover as an enhancement. Restoring tap-to-hover takes @custom-variant hover (&:hover), and that patch does nothing for keyboard users, a gap that matters most in hover-open menus.

For site navigation, a sensible default is to build both modes and ship click, keeping hover only for clients who insist.

Hover has a second failure mode. A margin between trigger and panel leaves a dead zone where the pointer exits the hover target and the menu closes, so put padding on a wrapper around the panel instead of mt-2 on the panel.

How to add click toggling with Alpine.js and Headless UI

Alpine.js adds click toggling with an open flag, x-show and a click.outside handler. Headless UI wraps the same behavior in React components and adds keyboard handling and focus management on top.

Alpine.js click dropdown

The wrapper gets x-data=”{ open: false }” to hold the state. The trigger button gets @click=”open = !open” to flip it. The panel takes x-show=”open” with @click.outside=”open = false”, so it appears on click and closes when a click lands elsewhere.

Escape needs one more handler, @keydown.escape.window=”open = false”. The window modifier attaches the listener to the page, so Escape works wherever focus sits.

The Alpine.js docs state that click.outside is only evaluated while its element is visible. That avoids a race where the trigger click would also fire the outside handler.

Alpine does not manage arrow keys, focus return or ARIA attributes. Add them by hand or move to a headless library.

Headless UI Menu in React and Next.js

The Headless UI documentation (v2.1, as of October 2026) builds the menu out of Menu, MenuButton, MenuItems and MenuItem.

Enter, Space or an arrow key on the button opens the list, Escape closes it, and Home, End and typed letters move focus between items. Styling hooks arrive as data attributes, so data-focus:bg-blue-100 on a MenuItem highlights the active entry.

A Tailwind setup inside a React project only needs npm install @headlessui/react to start.

Positioning comes from the anchor prop. Version 2.0 integrated Floating UI directly, so anchor=”bottom start” keeps the panel in view, and –anchor-gap sets the spacing.

One default deserves attention. MenuItems sets modal to true, which locks scroll, traps focus and makes the rest of the page inert while the menu is open. Turn it off if the menu should leave the page interactive.

On Next.js 13 and later, Link works inside a MenuItem as is. Next.js 12 and older need a wrapper that forwards unknown props to the anchor, or the menu stays open after a click.

Headless UI in Vue

Vue support lags behind React. As of October 2026, the Vue link on the Headless UI site points to the v1 documentation path, while the React docs cover v2.1 with the anchor prop and data attributes.

Copy React v2 examples into Vue for the Tailwind classes only, not for the component API.

A framework-free alternative is Tailwind Plus Elements, which Tailwind Labs says works anywhere a script tag loads. It still requires the Plus license.

Making a Tailwind dropdown accessible

What an accessible dropdown needs depends on what’s inside the panel. Actions call for a menu button that announces its state and handles the keyboard the way the pattern says it should, while a list of site links is a different case. Either way, focus has to stay visible.

The W3C WAI-ARIA Authoring Practices menu button pattern covers action menus.

  • The trigger gets role button, with aria-haspopup set to menu or true.
  • aria-expanded is true while the panel is open and false while it’s hidden.
  • The panel gets role menu, and each entry inside it gets role menuitem.
  • aria-controls pointing at the panel is optional.

On the button, Enter and Space open the menu and move focus to its first item. Down Arrow or Up Arrow can do the same for the first or last item.

Writing those ARIA attributes by hand is where custom builds usually slip. A details and summary build exposes disclosure semantics only, so menu roles and the arrow-key script must be added manually.

Site navigation is where the sources split. The APG Navigation Menubar pattern draws criticism from accessibility specialists, and Adrian Roselli argues against ARIA menu roles for site nav altogether, because a misapplied role makes assistive technology misread the interface (Evinced, 2021).

Sarah Higley’s disclosure pattern sits in the middle, and the APG now includes a disclosure navigation menu example as another option. It is a button with aria-expanded, plain links inside, and Escape to close.

The workable rule across these sources is role menu for actions and disclosure for lists of links.

WCAG 1.4.13 (Level AA) sets conditions for content revealed by hover or focus. It has to be dismissible and hoverable, and it has to stay persistent (W3C).

For a hover-opened panel, dismissible means Escape closes it without moving the pointer. Most web accessibility audits check this, and a hover dropdown with no Escape handler fails it outright.

Keep a visible focus ring on the trigger and on every item, using focus-visible variants instead of stripping outlines.

Animating and theming the open and close state

Animate opacity and scale from the panel’s anchor corner. Use transition and origin-top-right, with scale-95 and opacity-0 in the closed state, and make the enter slower than the leave.

Tailwind Labs’ own dropdown markup uses 100 ms for the enter (ease-out) and 75 ms for the leave (ease-in), per the Tailwind Plus Elements example from 2025. At those durations the toggle works as a micro-interaction that users barely register.

  • transition-discrete is required when the panel switches between hidden and block. It sets transition-behavior to allow-discrete, and without it the panel snaps instead of fading (Tailwind CSS docs, v4.3).
  • In Headless UI, the transition prop on MenuItems adds data-closed, data-enter and data-leave attributes, which Tailwind styles through data-closed: variants.
  • Wrap transition classes in motion-safe: so people with a reduced-motion setting get an instant toggle.

Dark mode follows prefers-color-scheme by default through the dark variant, and a custom variant switches it to a class or a data attribute (Tailwind CSS docs).

Give the panel, the item text and the focus highlight each a dark: counterpart. A forgotten highlight leaves light text on a light row, so check color contrast in both themes.

Put the dark class or data-theme attribute on the html element. Headless UI sets portal to true whenever anchor is used, so the panel renders outside your app wrapper and styles keyed to a wrapper div never reach it.

Why a Tailwind dropdown gets clipped or hidden behind other elements

Usually an ancestor is clipping the panel, a stacking context is trapping its z-index, or the panel runs past the viewport edge.

SymptomCauseFix
Panel cut off at a container edgeAncestor with overflow-hidden or overflow-autoRemove the overflow or move the panel to the top layer
Panel renders behind a siblingz-index only counts inside its parent stacking contextRaise z-index on the ancestor, not the panel
z-index has no effectAn ancestor sets transform, opacity below 1, filter or isolateRemove that property or hoist the panel out
Panel runs off the screenNo collision handlingFlip or shift the panel, or add position fallbacks

Per MDN, a child’s z-index only has meaning inside its parent stacking context, and each of those properties creates one. In Tailwind terms, that means isolate, opacity-90, scale-* and blur-* on an ancestor.

The fixes run from least to most JavaScript.

  • The popover attribute renders the panel in the top layer, unaffected by parent position or overflow, and an auto popover closes on outside click or Escape (MDN).
  • With CSS anchor positioning, position-try-fallbacks lets the browser retry another position when the first one overflows (MDN). It has been Baseline Newly available since January 2026, though Chrome only accepts that name from version 128 (125 to 127 used position-try-options).
  • Floating UI’s flip and shift middleware keep the panel inside its clipping container, and its fixed strategy in many cases lifts the panel out of a clipping ancestor (Floating UI docs).

MDN now lists the popover attribute as Baseline Widely available, with cross-browser support dated to April 2024.

Anchor positioning support is no longer the blocker it was. web.dev lists CSS anchor positioning as Baseline Newly available since Firefox 147 shipped in January 2026 (Chrome 125 and Safari 26 already supported it), and MDN marks position-try-fallbacks as Baseline 2026, newly available. Older daisyUI documentation, including the archived v5 docs site, still said Firefox and Safari did not support it, but the current daisyUI dropdown page no longer carries that warning.

Edge cases remain. The web-features dataset behind Baseline now tracks newer parts of the spec that are not yet in every browser, so check Can I use and MDN’s compatibility table for the exact properties you rely on, and keep Floating UI as the fallback for older browsers and cross-browser compatibility.

What breaks when a dropdown moves from Tailwind v3 to v4

Run the upgrade tool in a branch, then test every open state by hand (Tailwind CSS upgrade guide, v4.3 docs). Dropdown markup gets hit in a handful of places.

  • Transform transitions changed. transition-[opacity,transform] no longer animates scale, so use transition-[opacity,scale]. transform-none no longer resets scale either, so use scale-none.
  • Focus styles moved. outline-none now sets outline-style none, and outline-hidden keeps the invisible outline that shows in forced colors mode. The ring utility shrank from 3px to 1px and its default color moved from blue-500 to currentColor, so ring-3 plus an explicit color restores v3.
  • The shadow and radius scale shifted down a step. shadow-sm became shadow-xs, shadow became shadow-sm, and rounded follows the same shift. The bare names still compile but look different.
  • border-* and divide-* now default to currentColor, so item dividers turn dark unless they name a color.
  • Preflight gives buttons the default cursor instead of pointer, which affects trigger buttons and button items.
  • Block or flex classes no longer override the hidden attribute, so a panel toggled with the attribute must have the attribute removed.

The browser floor moved as well. Tailwind CSS v4.0 targets Safari 16.4, Chrome 111 and Firefox 128, and sites that need older browsers stay on v3.4 (Tailwind CSS upgrade guide).

The oldest Chrome and Safari versions in that floor predate the popover attribute, so a top-layer dropdown needs a fallback there. Tailwind Labs bundles polyfills in Elements for this reason, so it runs on the browsers v4.0 supports.

Tailwind Dropdown FAQ

Is the Tailwind Plus dropdown free?

No. Tailwind Plus is a paid product, and its documentation states that Tailwind Plus Elements requires a commercial license.

Open-source alternatives are Headless UI, Flowbite, Preline UI and daisyUI.

How do you build a nested dropdown?

Place a second details element inside the parent panel, or nest popover elements. MDN documents that an auto popover stays open while a nested popover opens.

Hover-opened submenus must meet the WCAG 1.4.13 conditions at every level.

What is the difference between a Menu and a Listbox?

A Menu runs actions or opens links. A Listbox picks a value that stays selected, like a form field.

Tailwind Plus Elements mirrors the split with el-dropdown for menus and el-select for form-style selects.

Does a dropdown work inside a mobile navbar?

Yes, as an inline expanding section instead of a floating panel. Headless UI and Tailwind Plus Elements both ship a disclosure component for this.

In a Tailwind navbar, Tailwind Plus uses disclosures for the mobile menu.

Do Flowbite, Preline and daisyUI dropdowns work with Tailwind v4?

Yes. Flowbite version 3.0.0 upgraded to Tailwind v4, and Flowbite React detects v3 and v4 automatically. daisyUI 5 targets v4.

Preline’s current install guide uses the v4 CSS import syntax.

Can a dropdown include icons, dividers and section headings?

Yes. In Headless UI, MenuSeparator draws the divider, and MenuSection with a MenuHeading adds labeled groups (Headless UI documentation).

Add icons as inline SVG in HTML marked aria-hidden, so screen readers announce only the item text.

What to Test Before a Tailwind Dropdown Ships

A Tailwind dropdown is ready to ship once it survives a keyboard-only pass, a tap on a touch device, a trip through clipped containers and a production build on Tailwind v4.

The order matters because each check changes what the next one shows.

  • Start with the production v4 build, since renamed utilities alter every later result.
  • Then go keyboard-only. It needs no extra device and catches the cheapest failures.
  • Finish on a real phone inside the real page layout, where tap and clipping bugs surface.

This sequence is verified as of October 29, 2026. CSS anchor positioning is already Baseline Newly available, so the Floating UI fallback and its extra JavaScript can come out of the build once your browser targets are Chrome 128, Safari 26 and Firefox 147 or newer.

Overlay components such as a Tailwind modal face the same passes, with focus trapping added.