Dropdown
Accessible dropdown menu with click-outside detection, keyboard support, section labels, dividers, and icon-ready menu items.
Preview
Basic dropdown
import { Dropdown, DropdownButton, DropdownMenu, DropdownItem, DropdownDivider } from '@/registry/ui-kit/tailwind-dropdown/react'
function Example() {
return (
<Dropdown>
<DropdownButton className="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white">
Options
</DropdownButton>
<DropdownMenu>
<DropdownItem onClick={() => {}}>Edit</DropdownItem>
<DropdownItem onClick={() => {}}>Duplicate</DropdownItem>
<DropdownDivider />
<DropdownItem onClick={() => {}}>Archive</DropdownItem>
<DropdownItem onClick={() => {}}>Delete</DropdownItem>
</DropdownMenu>
</Dropdown>
)
}Component API
| Prop | Default | Description |
|---|---|---|
Dropdown | — | Wrapper that provides open/close state and click-outside detection. |
DropdownButton | — | Trigger button that toggles the menu. |
DropdownMenu | — | Positioned menu container rendered below the trigger. |
DropdownItem | — | Menu item — renders as a button or anchor when href is provided. |
DropdownDivider | — | Horizontal rule to separate menu sections. |
DropdownLabel | — | Non-interactive section heading inside the menu. |
Source Code
"use client";
import React, { useState, useRef, useEffect, createContext, useContext } from "react";
interface DropdownContextValue {
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
const DropdownContext = createContext<DropdownContextValue>({
open: false,
setOpen: () => {},
});
/* ── Dropdown (wrapper) ─────────────────────────────── */
export function Dropdown({
children,
className = "",
}: {
children: React.ReactNode;
className?: string;
}) {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
// Click outside
useEffect(() => {
function handleClick(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
}
}
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, []);
// Escape key
useEffect(() => {
function handleKey(e: KeyboardEvent) {
if (e.key === "Escape") setOpen(false);
}
if (open) {
document.addEventListener("keydown", handleKey);
return () => document.removeEventListener("keydown", handleKey);
}
}, [open]);
return (
<DropdownContext.Provider value={{ open, setOpen }}>
<div ref={ref} className={`relative inline-block ${className}`}>
{children}
</div>
</DropdownContext.Provider>
);
}
/* ── DropdownButton ─────────────────────────────────── */
export function DropdownButton({
children,
className = "",
}: {
children: React.ReactNode;
className?: string;
}) {
const { open, setOpen } = useContext(DropdownContext);
return (
<button
type="button"
onClick={() => setOpen(!open)}
className={className}
aria-expanded={open}
aria-haspopup="true"
>
{children}
</button>
);
}
/* ── DropdownMenu ───────────────────────────────────── */
export function DropdownMenu({
children,
className = "",
}: {
children: React.ReactNode;
className?: string;
}) {
const { open } = useContext(DropdownContext);
if (!open) return null;
return (
<div
role="menu"
className={`absolute left-0 z-10 mt-1 min-w-48 rounded-lg border border-zinc-200 bg-white p-1 shadow-lg dark:border-zinc-700 dark:bg-zinc-900 ${className}`}
>
{children}
</div>
);
}
/* ── DropdownItem ───────────────────────────────────── */
export function DropdownItem({
children,
href,
onClick,
className = "",
}: {
children: React.ReactNode;
href?: string;
onClick?: () => void;
className?: string;
}) {
const { setOpen } = useContext(DropdownContext);
const baseClass = `flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800 ${className}`;
if (href) {
return (
<a href={href} role="menuitem" className={baseClass} onClick={() => setOpen(false)}>
{children}
</a>
);
}
return (
<button
type="button"
role="menuitem"
className={baseClass}
onClick={() => {
onClick?.();
setOpen(false);
}}
>
{children}
</button>
);
}
/* ── DropdownDivider ────────────────────────────────── */
export function DropdownDivider() {
return <hr className="my-1 border-zinc-100 dark:border-zinc-800" />;
}
/* ── DropdownLabel ──────────────────────────────────── */
export function DropdownLabel({
children,
className = "",
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<p className={`px-3 py-1.5 text-xs font-medium text-zinc-400 dark:text-zinc-500 ${className}`}>
{children}
</p>
);
}