Dropdown Components

Beautiful, responsive dropdown components built with TailwindCSS. Perfect for menus, actions, and navigation. Copy, paste, and customize for React, Vue, or HTML.

1) Basic Profile Dropdown

Opens on click, closes on outside click / Esc. Uses Heroicons-style inline SVG.

<div class="relative" x-data="{ open:false }" @keydown.escape="open=false">
  <button type="button" @click="open=!open" :aria-expanded="open" aria-haspopup="menu" class="inline-flex items-center gap-2 rounded-lg border border-black/10 bg-white px-4 py-2 text-sm font-medium shadow-sm hover:bg-gray-50">
    <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-700" viewBox="0 0 24 24" fill="none" stroke="currentColor">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0ZM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
    </svg> Account
    <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 9-7 7-7-7"/>
    </svg>
  </button>

  <div x-cloak x-show="open" @click.outside="open=false" @click.away="open=false" x-transition.opacity role="menu" aria-label="Profile" class="absolute right-0 mt-2 min-w-[12rem] rounded-lg border border-black/10 bg-white p-1.5 shadow-lg ring-1 ring-black/5 z-50">
    <a href="#" role="menuitem" class="flex items-center gap-3 rounded-md px-3 py-2 text-sm hover:bg-gray-50">... Settings</a>
    <a href="#" role="menuitem" class="flex items-center gap-3 rounded-md px-3 py-2 text-sm hover:bg-gray-50">... Security</a>
    <div class="my-1 h-px bg-gray-100"></div>
    <a href="#" role="menuitem" class="flex items-center gap-3 rounded-md px-3 py-2 text-sm text-red-600 hover:bg-red-50">... Sign out</a>
  </div>
</div>
<template>
  <div class="relative inline-block">
    <button @click="open=!open" @keydown.esc="open=false" :aria-expanded="open" aria-haspopup="menu" class="inline-flex items-center gap-2 rounded-lg border border-black/10 bg-white px-4 py-2 text-sm font-medium shadow-sm hover:bg-gray-50">
      <!-- icons --> Account
    </button>
    <div v-show="open" role="menu" aria-label="Profile" class="absolute right-0 mt-2 min-w-[12rem] rounded-lg border border-black/10 bg-white p-1.5 shadow-lg ring-1 ring-black/5">
      <button @click="open=false" role="menuitem" class="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm hover:bg-gray-50">Settings</button>
      <button @click="open=false" role="menuitem" class="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm hover:bg-gray-50">Security</button>
      <div class="my-1 h-px bg-gray-100" />
      <button @click="open=false" role="menuitem" class="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm text-red-600 hover:bg-red-50">Sign out</button>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
const open = ref(false)
const root = ref(null)
function onDocClick(e){ if(root.value && !root.value.contains(e.target)) open.value=false }
function onKey(e){ if(e.key==='Escape') open.value=false }
onMounted(()=>{ document.addEventListener('click', onDocClick); document.addEventListener('keydown', onKey) })
onBeforeUnmount(()=>{ document.removeEventListener('click', onDocClick); document.removeEventListener('keydown', onKey) })
</script>
import React, { useEffect, useRef, useState } from 'react'
export default function ProfileDropdown(){
  const [open,setOpen]=useState(false); const ref=useRef(null)
  useEffect(()=>{ const onDoc=e=>{if(ref.current && !ref.current.contains(e.target)) setOpen(false)}
                  const onKey=e=>{if(e.key==='Escape') setOpen(false)}
                  document.addEventListener('click',onDoc); document.addEventListener('keydown',onKey)
                  return ()=>{document.removeEventListener('click',onDoc);document.removeEventListener('keydown',onKey)}},[])
  return (<div className="relative inline-block" ref={ref}>
    <button onClick={()=>setOpen(v=>!v)} aria-haspopup="menu" aria-expanded={open} className="inline-flex items-center gap-2 rounded-lg border border-black/10 bg-white px-4 py-2 text-sm font-medium shadow-sm hover:bg-gray-50">Account</button>
    {open && (<div role="menu" aria-label="Profile" className="absolute right-0 mt-2 min-w-[12rem] rounded-lg border border-black/10 bg-white p-1.5 shadow-lg ring-1 ring-black/5">
      <button role="menuitem" className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm hover:bg-gray-50">Settings</button>
      <button role="menuitem" className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm hover:bg-gray-50">Security</button>
      <div className="my-1 h-px bg-gray-100" />
      <button role="menuitem" className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm text-red-600 hover:bg-red-50">Sign out</button>
    </div>)}
  </div>)
}

2) Split Actions Dropdown

Primary action + caret. Includes a danger section.

<div class="inline-flex items-center">
  <button type="button" class="rounded-l-lg bg-violet px-4 py-2 text-sm font-semibold text-white border border-black/10 hover:brightness-95">Save</button>
  <div class="relative" x-data="{ open:false }" @keydown.escape="open=false">
    <button type="button" @click="open=!open" :aria-expanded="open" aria-haspopup="menu" class="rounded-r-lg bg-violet/90 px-2 py-2 text-white border border-black/10 hover:brightness-95" title="More actions">
      <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 9-7 7-7-7"/>
      </svg>
    </button>
    <div x-cloak x-show="open" @click.outside="open=false" @click.away="open=false" x-transition.opacity role="menu" aria-label="More actions" class="absolute right-0 top-full mt-2 min-w-[14rem] rounded-lg border border-black/10 bg-white p-1.5 shadow-lg ring-1 ring-black/5 z-50">
      <button role="menuitem" class="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm hover:bg-gray-50">Duplicate</button>
      <button role="menuitem" class="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm hover:bg-gray-50">Share</button>
      <div class="my-1 h-px bg-gray-100"></div>
      <button role="menuitem" class="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm text-red-600 hover:bg-red-50">Delete</button>
    </div>
  </div>
</div>
<template>
  <div class="inline-flex items-center">
    <button class="rounded-l-lg bg-violet px-4 py-2 text-sm font-semibold text-white border border-black/10 hover:brightness-95">Save</button>
    <div class="relative">
      <button @click="open=!open" :aria-expanded="open" aria-haspopup="menu" class="rounded-r-lg bg-violet/90 px-2 py-2 text-white border border-black/10 hover:brightness-95">▼</button>
      <div v-show="open" role="menu" aria-label="More actions" class="absolute right-0 top-full mt-2 min-w-[14rem] rounded-lg border border-black/10 bg-white p-1.5 shadow-lg ring-1 ring-black/5">
        <button role="menuitem" class="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm hover:bg-gray-50">Duplicate</button>
        <button role="menuitem" class="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm hover:bg-gray-50">Share</button>
        <div class="my-1 h-px bg-gray-100" />
        <button role="menuitem" class="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm text-red-600 hover:bg-red-50">Delete</button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const open = ref(false)
</script>
import React, { useEffect, useRef, useState } from 'react'
export default function SplitActionsDropdown(){
  const [open,setOpen]=useState(false); const ref=useRef(null)
  useEffect(()=>{ const onDoc=e=>{if(ref.current && !ref.current.contains(e.target)) setOpen(false)}
                  const onKey=e=>{if(e.key==='Escape') setOpen(false)}
                  document.addEventListener('click',onDoc); document.addEventListener('keydown',onKey)
                  return ()=>{document.removeEventListener('click',onDoc);document.removeEventListener('keydown',onKey)}},[])
  return (<div className="inline-flex items-center">
    <button className="rounded-l-lg bg-violet px-4 py-2 text-sm font-semibold text-white border border-black/10 hover:brightness-95">Save</button>
    <div className="relative" ref={ref}>
      <button onClick={()=>setOpen(v=>!v)} aria-haspopup="menu" aria-expanded={open} className="rounded-r-lg bg-violet/90 px-2 py-2 text-white border border-black/10 hover:brightness-95" title="More actions">▼</button>
      {open && (<div role="menu" aria-label="More actions" className="absolute right-0 top-full mt-2 min-w-[14rem] rounded-lg border border-black/10 bg-white p-1.5 shadow-lg ring-1 ring-black/5">
        <button role="menuitem" className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm hover:bg-gray-50">Duplicate</button>
        <button role="menuitem" className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm hover:bg-gray-50">Share</button>
        <div className="my-1 h-px bg-gray-100" />
        <button role="menuitem" className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm text-red-600 hover:bg-red-50">Delete</button>
      </div>)}
    </div>
  </div>)
}

Notes

  • Previews avoid clipping: no overflow-hidden, added preview-stage, and menus use z-50.
  • For Alpine outside click, both @click.outside and @click.away are included for compatibility.
  • Add Alpine in your Vite app.js once (no CDN duplication).

We use cookies to improve your experience and analytics. You can accept all cookies or reject non-essential ones.

Learn More