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.
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
Open the menu to switch workspaces.
Show sourceexamples/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.
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
"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>
);
}Submenu and descriptions
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
"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.
| Prop | What it does | Default |
|---|---|---|
open / defaultOpen / onOpenChangeboolean / boolean / (open) => void | Controlled or uncontrolled menu visibility. | Not set |
motionPreset / reducedMotion"none" | "subtle" | "standard" / boolean | Motion 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() => void | Runs when the item is selected by click or keyboard. | Not set |
DropdownMenuItem — destructive / disabledboolean | Destructive styling with data-destructive, or a disabled non-interactive item. | false |
Item anatomyIcon | Label | Description | Shortcut | DropdownMenuItemIcon/Label/Description/Shortcut compose rich rows inside an item. | Not set |
StructureSection | Label | Separator | Submenu | DropdownMenuSection 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.