Skip to content
Dethink Components

ComponentsDropdownMenu

DropdownMenu

Show a menu of actions when users open a trigger.

On this page

Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.

View registry files
  • Motion for React (motion)

    Animations, transitions, and reduced-motion handling.

  • React Aria (react-aria)

    Accessibility hooks and overlay utilities used by the implementation.

  • React Aria Components (react-aria-components)

    Accessible interaction primitives, including keyboard behavior, focus handling, and ARIA semantics.

Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.

Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.

Import the component into your page or component file.

Usage
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuItemLabel,
  DropdownMenuSection,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@dethink/components";

Try the examples, then open the code to use them in your app. Open a menu and navigate with arrows, Home/End, or by typing an item name.

Workspace switcher

A compact workspace card with initials, plan details, and an accessible active selection. Choose a workspace to update the card.

Your workspace

One place for your next big idea.

Open the menu to switch workspaces.

Show sourceexamples/dropdown-menu/workspace.tsx
examples/dropdown-menu/workspace.tsx
"use client";

import { useState } from "react";
import { Check, ChevronsUpDown, Layers3, Plus, Sparkles } from "lucide-react";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuItemDescription,
  DropdownMenuItemLabel,
  DropdownMenuLabel,
  DropdownMenuSection,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@dethink/components";

const workspaces = [
  {
    id: "studio",
    name: "Acme Studio",
    detail: "Pro plan · 12 members",
    initials: "AS",
  },
  {
    id: "product",
    name: "Product Lab",
    detail: "Team plan · 8 members",
    initials: "PL",
  },
  {
    id: "personal",
    name: "Personal",
    detail: "Free plan · Just you",
    initials: "ME",
  },
];

export function DropdownMenuWorkspace() {
  const [active, setActive] = useState(workspaces[0]);
  const [status, setStatus] = useState("");
  return (
    <div className="mx-auto w-full max-w-sm space-y-4">
      <div className="border-border bg-background rounded-2xl border p-5 shadow-sm">
        <p className="text-muted-foreground mb-4 text-xs font-medium tracking-widest uppercase">
          Your workspace
        </p>
        <DropdownMenu>
          <DropdownMenuTrigger
            aria-label={`Switch workspace, ${active.name}`}
            variant="ghost"
            className="border-border bg-muted/30 h-auto min-h-16 w-full justify-start gap-3 rounded-xl p-3 text-start [&_[data-slot=button-content]]:w-full [&_[data-slot=button-label]]:w-full"
          >
            <span className="flex w-full items-center gap-3">
              {" "}
              <span className="bg-primary text-primary-foreground flex size-10 shrink-0 items-center justify-center rounded-lg text-sm font-semibold">
                {active.initials}
              </span>
              <span className="min-w-0 flex-1">
                <span className="block truncate text-sm font-semibold">
                  {active.name}
                </span>
                <span className="text-muted-foreground block text-xs font-normal">
                  {active.detail}
                </span>
              </span>
              <ChevronsUpDown
                aria-hidden="true"
                className="text-muted-foreground size-4 shrink-0"
              />
            </span>
          </DropdownMenuTrigger>
          <DropdownMenuContent
            aria-label="Workspaces"
            className="max-h-none w-72 rounded-xl p-1.5"
            menuClassName="max-h-[min(26rem,70dvh)]"
          >
            <DropdownMenuSection
              aria-label="Available workspaces"
              selectionMode="single"
              disallowEmptySelection
              selectedKeys={[active.id]}
              onSelectionChange={(keys) => {
                const next = workspaces.find(
                  (workspace) => keys !== "all" && keys.has(workspace.id),
                );
                if (next) {
                  setActive(next);
                  setStatus(`Switched to ${next.name}.`);
                }
              }}
            >
              <DropdownMenuLabel className="px-3 py-2 text-[11px] tracking-wider">
                Switch workspace
              </DropdownMenuLabel>
              {workspaces.map((workspace) => (
                <DropdownMenuItem
                  id={workspace.id}
                  key={workspace.id}
                  textValue={workspace.name}
                  className="grid-cols-[2.25rem_minmax(0,1fr)_1rem] rounded-lg px-3 py-3"
                >
                  {({ isSelected }) => (
                    <>
                      <span
                        aria-hidden="true"
                        className={`row-span-2 flex size-9 items-center justify-center rounded-lg border text-xs font-semibold ${isSelected ? "border-primary/20 bg-primary/10 text-primary" : "border-border bg-background text-muted-foreground"}`}
                      >
                        {workspace.initials}
                      </span>
                      <DropdownMenuItemLabel className="font-medium">
                        {workspace.name}
                      </DropdownMenuItemLabel>
                      <span className="text-primary row-span-2">
                        {isSelected && (
                          <Check aria-hidden="true" className="size-4" />
                        )}
                      </span>
                      <DropdownMenuItemDescription>
                        {workspace.detail}
                      </DropdownMenuItemDescription>
                    </>
                  )}
                </DropdownMenuItem>
              ))}
            </DropdownMenuSection>
            <DropdownMenuSeparator />
            <DropdownMenuSection aria-label="Workspace actions">
              <DropdownMenuItem
                textValue="Create workspace"
                className="rounded-lg px-3 py-2.5"
                onAction={() =>
                  setStatus("Create workspace selected. This is a local demo.")
                }
              >
                <Plus
                  aria-hidden="true"
                  className="text-muted-foreground size-4"
                />
                <DropdownMenuItemLabel>Create workspace</DropdownMenuItemLabel>
              </DropdownMenuItem>
            </DropdownMenuSection>
          </DropdownMenuContent>
        </DropdownMenu>
        <div className="text-muted-foreground mt-5 flex items-center gap-2 text-xs">
          <Layers3 aria-hidden="true" className="size-4" />
          <span>One place for your next big idea.</span>
          <Sparkles
            aria-hidden="true"
            className="text-primary ms-auto size-4"
          />
        </div>
      </div>
      <p
        role="status"
        className="text-muted-foreground min-h-5 text-center text-xs"
      >
        {status || "Open the menu to switch workspaces."}
      </p>
    </div>
  );
}

Account menu

A profile header, grouped settings, and a highlighted plan action. Actions show local demo feedback.

Your profile, preferences, and a little more.

Show sourceexamples/dropdown-menu/account.tsx
examples/dropdown-menu/account.tsx
"use client";

import { useState } from "react";
import {
  ArrowUpRight,
  Bell,
  ChevronDown,
  CreditCard,
  LifeBuoy,
  LogOut,
  Settings2,
  Sparkles,
  UserRound,
} from "lucide-react";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuItemIcon,
  DropdownMenuItemLabel,
  DropdownMenuItemDescription,
  DropdownMenuLabel,
  DropdownMenuSection,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@dethink/components";

const actions = [
  { label: "My profile", icon: UserRound },
  { label: "Preferences", icon: Settings2 },
  { label: "Notifications", icon: Bell },
  { label: "Billing", icon: CreditCard },
];

export function DropdownMenuAccount() {
  const [status, setStatus] = useState("");
  const choose = (label: string) =>
    setStatus(`${label} selected. This is a local demo.`);
  return (
    <div className="flex flex-col items-center gap-5">
      <DropdownMenu>
        <DropdownMenuTrigger
          aria-label="Alex Morgan account"
          variant="outline"
          className="h-auto min-h-14 gap-3 rounded-full py-2 ps-2 pe-4 shadow-sm"
        >
          <span className="flex w-full items-center gap-3">
            {" "}
            <span
              aria-hidden="true"
              className="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-full text-sm font-semibold"
            >
              AM
            </span>
            <span className="text-start">
              <span className="block text-sm font-semibold">Alex Morgan</span>
              <span className="text-muted-foreground block text-xs font-normal">
                Personal account
              </span>
            </span>
            <ChevronDown
              aria-hidden="true"
              className="text-muted-foreground ms-3 size-4"
            />
          </span>
        </DropdownMenuTrigger>
        <DropdownMenuContent
          aria-label="Account"
          placement="bottom end"
          className="max-h-none w-72 rounded-xl p-1.5"
          menuClassName="max-h-[min(32rem,70dvh)]"
        >
          <DropdownMenuSection>
            <DropdownMenuLabel className="p-3 font-normal normal-case">
              <span className="flex items-center gap-3">
                <span
                  aria-hidden="true"
                  className="bg-primary/10 text-primary flex size-10 shrink-0 items-center justify-center rounded-full font-semibold"
                >
                  AM
                </span>
                <span className="min-w-0">
                  <span className="text-foreground block text-sm font-semibold">
                    Alex Morgan
                  </span>
                  <span className="block truncate text-xs">
                    alex@example.com
                  </span>
                </span>
                <span className="border-primary/20 bg-primary/10 text-primary ms-auto rounded-full border px-2 py-0.5 text-[10px] font-semibold">
                  PRO
                </span>
              </span>
            </DropdownMenuLabel>
            {actions.map(({ label, icon: Icon }) => (
              <DropdownMenuItem
                key={label}
                textValue={label}
                onAction={() => choose(label)}
                className="rounded-lg px-3 py-2.5"
              >
                <DropdownMenuItemIcon>
                  <Icon aria-hidden="true" />
                </DropdownMenuItemIcon>
                <DropdownMenuItemLabel>{label}</DropdownMenuItemLabel>
              </DropdownMenuItem>
            ))}
          </DropdownMenuSection>
          <DropdownMenuSeparator />
          <DropdownMenuSection aria-label="Plan">
            <DropdownMenuItem
              textValue="Explore team plan"
              onAction={() => choose("Team plan")}
              className="border-primary/15 bg-primary/5 m-1 rounded-lg border px-3 py-3"
            >
              <DropdownMenuItemIcon className="text-primary">
                <Sparkles aria-hidden="true" />
              </DropdownMenuItemIcon>
              <DropdownMenuItemLabel className="font-medium">
                Better together
              </DropdownMenuItemLabel>
              <ArrowUpRight
                aria-hidden="true"
                className="text-primary size-4"
              />
              <DropdownMenuItemDescription>
                Explore the Team plan
              </DropdownMenuItemDescription>
            </DropdownMenuItem>
          </DropdownMenuSection>
          <DropdownMenuSeparator />
          <DropdownMenuSection aria-label="Support and session">
            <DropdownMenuItem
              textValue="Help and support"
              onAction={() => choose("Help and support")}
              className="rounded-lg px-3 py-2.5"
            >
              <DropdownMenuItemIcon>
                <LifeBuoy aria-hidden="true" />
              </DropdownMenuItemIcon>
              <DropdownMenuItemLabel>Help & support</DropdownMenuItemLabel>
            </DropdownMenuItem>
            <DropdownMenuItem
              textValue="Sign out"
              destructive
              onAction={() => choose("Sign out")}
              className="rounded-lg px-3 py-2.5"
            >
              <DropdownMenuItemIcon>
                <LogOut aria-hidden="true" />
              </DropdownMenuItemIcon>
              <DropdownMenuItemLabel>Sign out</DropdownMenuItemLabel>
            </DropdownMenuItem>
          </DropdownMenuSection>
        </DropdownMenuContent>
      </DropdownMenu>
      <p
        role="status"
        className="text-muted-foreground min-h-5 text-center text-xs"
      >
        {status || "Your profile, preferences, and a little more."}
      </p>
    </div>
  );
}

Sections, icons, and shortcuts

A grouped menu with a label, icon and shortcut parts, a separator, and a destructive item.

Show sourceexamples/dropdown-menu/basic.tsx
examples/dropdown-menu/basic.tsx
"use client";

import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuItemIcon,
  DropdownMenuItemLabel,
  DropdownMenuItemShortcut,
  DropdownMenuLabel,
  DropdownMenuSection,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@dethink/components";
import { Copy, ExternalLink, RefreshCw, Trash2 } from "lucide-react";

export function DropdownMenuBasic() {
  return (
    <div className="flex justify-center">
      <DropdownMenu>
        <DropdownMenuTrigger variant="outline">
          Report actions
        </DropdownMenuTrigger>
        <DropdownMenuContent>
          <DropdownMenuSection>
            <DropdownMenuLabel>Report</DropdownMenuLabel>
            <DropdownMenuItem>
              <DropdownMenuItemIcon>
                <ExternalLink aria-hidden="true" />
              </DropdownMenuItemIcon>
              <DropdownMenuItemLabel>Open report</DropdownMenuItemLabel>
            </DropdownMenuItem>
            <DropdownMenuItem>
              <DropdownMenuItemIcon>
                <RefreshCw aria-hidden="true" />
              </DropdownMenuItemIcon>
              <DropdownMenuItemLabel>Refresh data</DropdownMenuItemLabel>
              <DropdownMenuItemShortcut>R</DropdownMenuItemShortcut>
            </DropdownMenuItem>
            <DropdownMenuItem>
              <DropdownMenuItemIcon>
                <Copy aria-hidden="true" />
              </DropdownMenuItemIcon>
              <DropdownMenuItemLabel>Duplicate</DropdownMenuItemLabel>
              <DropdownMenuItemShortcut>⌘D</DropdownMenuItemShortcut>
            </DropdownMenuItem>
            <DropdownMenuSeparator />
            <DropdownMenuItem destructive>
              <DropdownMenuItemIcon>
                <Trash2 aria-hidden="true" />
              </DropdownMenuItemIcon>
              <DropdownMenuItemLabel>Delete report</DropdownMenuItemLabel>
            </DropdownMenuItem>
          </DropdownMenuSection>
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
  );
}

DropdownMenuSubmenu nests a flyout; item descriptions add secondary context; disabled items stay visible but inert.

Examples that combine components for common tasks.

Row actions

One icon trigger per row, each labelled with the row's name for screen readers. onAction mutates the live list, the destructive item is styled as such, and a live region reports what happened.

  • production-api

    Last used 2 minutes ago

  • staging-api

    Last used 3 days ago

  • ci-deploy

    Last used just now

Show sourceexamples/dropdown-menu/recipe-row-actions.tsx
examples/dropdown-menu/recipe-row-actions.tsx
"use client";

import { useState } from "react";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuItemLabel,
  DropdownMenuSection,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@dethink/components";
import { MoreHorizontal } from "lucide-react";

const initialKeys = [
  { id: "k1", name: "production-api", lastUsed: "2 minutes ago" },
  { id: "k2", name: "staging-api", lastUsed: "3 days ago" },
  { id: "k3", name: "ci-deploy", lastUsed: "just now" },
];

/**
 * The menu's most common seat: one icon trigger per row, labelled with the
 * row's name so screen-reader users know which record the actions target.
 * Destructive items get the destructive treatment, and actions mutate the
 * live list.
 */
export function DropdownMenuRecipeRowActions() {
  const [keys, setKeys] = useState(initialKeys);
  const [status, setStatus] = useState("");

  return (
    <div className="mx-auto max-w-sm space-y-2">
      <ul className="divide-border border-border divide-y rounded-lg border">
        {keys.map((apiKey) => (
          <li
            key={apiKey.id}
            className="flex items-center justify-between gap-3 px-4 py-2.5"
          >
            <div>
              <p className="font-mono text-sm">{apiKey.name}</p>
              <p className="text-muted-foreground text-xs">
                Last used {apiKey.lastUsed}
              </p>
            </div>
            <DropdownMenu>
              <DropdownMenuTrigger
                variant="ghost"
                size="icon"
                aria-label={`Actions for ${apiKey.name}`}
              >
                <MoreHorizontal className="size-4" aria-hidden="true" />
              </DropdownMenuTrigger>
              <DropdownMenuContent placement="bottom end">
                <DropdownMenuSection>
                  <DropdownMenuItem
                    onAction={() => setStatus(`Copied ${apiKey.name}.`)}
                  >
                    <DropdownMenuItemLabel>Copy key</DropdownMenuItemLabel>
                  </DropdownMenuItem>
                  <DropdownMenuItem
                    onAction={() => setStatus(`Rotated ${apiKey.name}.`)}
                  >
                    <DropdownMenuItemLabel>Rotate…</DropdownMenuItemLabel>
                  </DropdownMenuItem>
                  <DropdownMenuSeparator />
                  <DropdownMenuItem
                    destructive
                    onAction={() => {
                      setKeys((current) =>
                        current.filter((entry) => entry.id !== apiKey.id),
                      );
                      setStatus(`Revoked ${apiKey.name}.`);
                    }}
                  >
                    <DropdownMenuItemLabel>Revoke</DropdownMenuItemLabel>
                  </DropdownMenuItem>
                </DropdownMenuSection>
              </DropdownMenuContent>
            </DropdownMenu>
          </li>
        ))}
      </ul>
      <p aria-live="polite" className="text-muted-foreground text-sm">
        {status}
      </p>
    </div>
  );
}

DropdownMenu coordinates trigger and content; items carry actions and states.

DropdownMenu anatomy
PropWhat it doesDefault
open / defaultOpen / onOpenChangeboolean / boolean / (open) => voidControlled or uncontrolled menu visibility.Not set
motionPreset / reducedMotion"none" | "subtle" | "standard" / booleanMotion owns surface presence and changed item feedback. Reduced motion removes transforms while preserving immediate state styling."standard" / user preference
DropdownMenuTriggervariant + size (Button API)The menu button; give icon-only triggers an aria-label naming their target.Not set
DropdownMenuContent — placement"bottom" | "top" | … (+ " start"/" end")Preferred side and alignment relative to the trigger."bottom"
DropdownMenuItem — onAction() => voidRuns when the item is selected by click or keyboard.Not set
DropdownMenuItem — destructive / disabledbooleanDestructive styling with data-destructive, or a disabled non-interactive item.false
Item anatomyIcon | Label | Description | ShortcutDropdownMenuItemIcon/Label/Description/Shortcut compose rich rows inside an item.Not set
StructureSection | Label | Separator | SubmenuDropdownMenuSection groups, DropdownMenuLabel titles a group, DropdownMenuSeparator divides, and DropdownMenuSubmenu + SubmenuContent nest a flyout.Not set

DropdownMenu now has one animation owner.

Surface entry and exit presence plus changed item feedback use primitives from motion/react. Registry installs now declare Motion as a runtime dependency. Tailwind overlay keyframes and transition utilities no longer run on the DropdownMenu path; the reducedMotion prop or user preference removes transform choreography without changing roles, focus, placement, portals, or submenu behavior.