Skip to content
Dethink Components

ComponentsDropdownMenu

DropdownMenu

Show a menu of actions when users open a trigger.

On this page

Use this component from the local workspace package. Follow the setup guide first. A public npm package and registry are not available yet.

View registry files

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.

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.