Dropdown Menu
Overlays & navigationRadix menu with origin-aware pop-in, 0.018s item cascade and sliding submenus.
Install
npx shadcn@latest add https://labs.duku.design/r/dropdown-menu.jsonAsk your agent to use this
Connect DUKU Labs to MCP, then hand your coding agent this prompt:
Use the DUKU Labs MCP server to install the Dropdown Menu component (get_component "dropdown-menu", then its install command). Adapt it to my existing design tokens, preserve all accessibility and reduced-motion behavior, and keep every interaction state working.
Interaction
| Try | What happens |
|---|---|
| Open the menu | Pops from the trigger origin; items cascade |
| Hover the submenu | Slides 8px from the trigger side |
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| destructive (Item) | boolean | false | Destructive item styling. |
Source — registry/default/ui/dropdown-menu.tsx
"use client";
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import { cn } from "@/lib/utils";
import { useReducedMotion } from "@/registry/default/lib/use-reduced-motion";
export const DropdownMenu = DropdownMenuPrimitive.Root;
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
export const DropdownMenuSub = DropdownMenuPrimitive.Sub;
export interface DropdownMenuContentProps
extends React.ComponentProps<typeof DropdownMenuPrimitive.Content> {
ref?: React.Ref<HTMLDivElement>;
}
export function DropdownMenuContent({
className,
sideOffset = 6,
ref,
...rest
}: DropdownMenuContentProps) {
const contentRef = React.useRef<HTMLDivElement>(null);
const reduced = useReducedMotion();
React.useImperativeHandle(ref, () => contentRef.current as HTMLDivElement);
useGSAP(
() => {
const el = contentRef.current;
if (!el) return;
if (reduced) {
gsap.set(el, { opacity: 1, scale: 1 });
return;
}
el.style.transformOrigin =
"var(--radix-dropdown-menu-content-transform-origin)";
gsap.fromTo(
el,
{ opacity: 0, scale: 0.96 },
{ opacity: 1, scale: 1, duration: 0.25, ease: "power3.out" }
);
const items = gsap.utils.toArray<HTMLElement>(
el.querySelectorAll('[data-slot="dropdown-menu-item"]')
);
if (items.length) {
gsap.fromTo(
items,
{ y: 4, opacity: 0 },
{
y: 0,
opacity: 1,
duration: 0.25,
ease: "power2.out",
stagger: 0.018,
}
);
}
},
{ scope: contentRef }
);
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={contentRef}
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[10rem] overflow-hidden rounded-lg border border-border bg-background p-1 opacity-0 shadow-lg",
className
)}
{...rest}
/>
</DropdownMenuPrimitive.Portal>
);
}
export interface DropdownMenuSubContentProps
extends React.ComponentProps<typeof DropdownMenuPrimitive.SubContent> {
ref?: React.Ref<HTMLDivElement>;
}
export function DropdownMenuSubContent({
className,
ref,
...rest
}: DropdownMenuSubContentProps) {
const contentRef = React.useRef<HTMLDivElement>(null);
const reduced = useReducedMotion();
React.useImperativeHandle(ref, () => contentRef.current as HTMLDivElement);
useGSAP(
() => {
const el = contentRef.current;
if (!el) return;
if (reduced) {
gsap.set(el, { opacity: 1, x: 0 });
return;
}
// slide 8px from the trigger side
const side = el.getAttribute("data-side") ?? "right";
const fromX = side === "left" ? 8 : -8;
gsap.fromTo(
el,
{ opacity: 0, x: fromX },
{ opacity: 1, x: 0, duration: 0.2, ease: "power3.out" }
);
},
{ scope: contentRef }
);
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.SubContent
ref={contentRef}
data-slot="dropdown-menu-sub-content"
sideOffset={4}
className={cn(
"z-50 min-w-[9rem] overflow-hidden rounded-lg border border-border bg-background p-1 opacity-0 shadow-lg",
className
)}
{...rest}
/>
</DropdownMenuPrimitive.Portal>
);
}
export interface DropdownMenuItemProps
extends React.ComponentProps<typeof DropdownMenuPrimitive.Item> {
destructive?: boolean;
ref?: React.Ref<HTMLDivElement>;
}
export function DropdownMenuItem({
className,
destructive = false,
ref,
...rest
}: DropdownMenuItemProps) {
return (
<DropdownMenuPrimitive.Item
ref={ref}
data-slot="dropdown-menu-item"
className={cn(
"flex cursor-default select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none",
"transition-colors duration-100",
"data-[highlighted]:bg-muted data-[disabled]:opacity-50 data-[disabled]:pointer-events-none",
destructive && "text-destructive data-[highlighted]:bg-destructive/10",
className
)}
{...rest}
/>
);
}
export interface DropdownMenuSubTriggerProps
extends React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> {
ref?: React.Ref<HTMLDivElement>;
}
export function DropdownMenuSubTrigger({
className,
children,
ref,
...rest
}: DropdownMenuSubTriggerProps) {
return (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
data-slot="dropdown-menu-item"
className={cn(
"flex cursor-default select-none items-center justify-between gap-2 rounded-md px-2 py-1.5 text-sm outline-none",
"transition-colors duration-100",
"data-[highlighted]:bg-muted data-[state=open]:bg-muted",
className
)}
{...rest}
>
{children}
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className="size-3.5 text-muted-foreground"
aria-hidden="true"
>
<path d="M9 6l6 6-6 6" />
</svg>
</DropdownMenuPrimitive.SubTrigger>
);
}
export function DropdownMenuLabel({
className,
...rest
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label>) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
className={cn(
"px-2 py-1.5 text-xs font-medium text-muted-foreground",
className
)}
{...rest}
/>
);
}
export function DropdownMenuSeparator({
className,
...rest
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("mx-1 my-1 h-px bg-border", className)}
{...rest}
/>
);
}
export function DropdownMenuShortcut({
className,
...rest
}: React.HTMLAttributes<HTMLSpanElement>) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...rest}
/>
);
}