Dropdown Menu
<div role="menu">
A floating panel of actions triggered by a button click. Closes on selection or outside click.
Overview
Preview
Basic dropdown
html
<!-- Trigger -->
<div class="relative inline-block">
<button class="inline-flex items-center gap-2 rounded-md bg-zinc-800 px-3 py-2 text-sm font-medium text-zinc-100 hover:bg-zinc-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-zinc-500">
Options
<svg class="h-4 w-4 text-zinc-400" ...>▾</svg>
</button>
<!-- Menu panel -->
<div class="absolute left-0 z-10 mt-1.5 w-48 origin-top-left rounded-lg border border-zinc-700/60 bg-zinc-900 py-1 shadow-xl shadow-black/40">
<button class="flex w-full items-center gap-2.5 px-3 py-2 text-sm text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100 focus:outline-none">
Profile
</button>
<button class="flex w-full items-center gap-2.5 px-3 py-2 text-sm text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100 focus:outline-none">
Settings
</button>
<div class="my-1 border-t border-zinc-800"/>
<button class="flex w-full items-center gap-2.5 px-3 py-2 text-sm text-red-400 hover:bg-red-950/30 hover:text-red-300 focus:outline-none">
Sign out
</button>
</div>
</div>Section labels
html
<div class="px-3 py-1.5 text-[10px] font-semibold uppercase tracking-widest text-zinc-600">Account</div>With icons
html
<button class="flex w-full items-center gap-2.5 px-3 py-2 text-sm text-zinc-300 hover:bg-zinc-800">
<svg class="h-3.5 w-3.5 shrink-0 text-zinc-500" ...></svg>
Profile
<span class="ml-auto text-[10px] text-zinc-600">⌘P</span>
</button>Destructive item
html
<button class="flex w-full items-center gap-2.5 px-3 py-2 text-sm text-red-400 hover:bg-red-950/30 hover:text-red-300 focus:outline-none">
<svg class="h-3.5 w-3.5 text-red-500" ...></svg>
Delete
</button>With Vue reactivity
vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const open = ref(false)
function onClickOutside(e) {
if (!e.target.closest('.dropdown-root')) open.value = false
}
onMounted(() => document.addEventListener('click', onClickOutside))
onUnmounted(() => document.removeEventListener('click', onClickOutside))
</script>
<template>
<div class="dropdown-root relative inline-block">
<button @click="open = !open" class="...">Options</button>
<Transition enter-active-class="transition ease-out duration-100" enter-from-class="opacity-0 scale-95" enter-to-class="opacity-100 scale-100">
<div v-if="open" class="absolute ...">
<!-- items -->
</div>
</Transition>
</div>
</template>With Headless UI (recommended)
vue
<script setup>
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
</script>
<template>
<Menu as="div" class="relative inline-block">
<MenuButton class="...">Options ▾</MenuButton>
<MenuItems class="absolute left-0 z-10 mt-1.5 w-48 rounded-lg border border-zinc-700/60 bg-zinc-900 py-1 shadow-xl focus:outline-none">
<MenuItem v-slot="{ active }">
<button :class="['flex w-full px-3 py-2 text-sm text-zinc-300', active && 'bg-zinc-800 text-zinc-100']">
Profile
</button>
</MenuItem>
</MenuItems>
</Menu>
</template>Accessibility notes
- Use
role="menu"on the panel,role="menuitem"on each button - Headless UI's
Menuautomatically manages focus, keyboard navigation (↑↓ arrows, Enter, Escape), and ARIA - Never use
<a>tags for actions — only for actual navigation