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

PropDefaultDescription
DropdownWrapper that provides open/close state and click-outside detection.
DropdownButtonTrigger button that toggles the menu.
DropdownMenuPositioned menu container rendered below the trigger.
DropdownItemMenu item — renders as a button or anchor when href is provided.
DropdownDividerHorizontal rule to separate menu sections.
DropdownLabelNon-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>
  );
}