Skip to content
EMBOSS
Docs menu

Dropdown Menu

A floating menu of actions with submenus, checkbox and radio items; the highlighted row presses momentarily into the surface.

@emboss/dropdown-menu

Installation

pnpm dlx shadcn@latest add @emboss/dropdown-menu

Usage

example.tsx
import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

export function Example() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button>Options</Button>} />
      <DropdownMenuContent>
        <DropdownMenuItem>Duplicate</DropdownMenuItem>
        <DropdownMenuItem variant="destructive">Delete</DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

API reference

DropdownMenu / DropdownMenuTrigger / DropdownMenuContent

Root state, trigger, and the floating list.

PropTypeDefaultDescription
sideOffsetnumber6Gap between trigger and menu (DropdownMenuContent).

DropdownMenuItem

PropTypeDefaultDescription
variant"default" | "destructive""default"Destructive items carry danger ink.
onClickMouseEventHandlerRuns when the item is activated.
Data attributeDescription
data-highlightedPresent on the virtually focused item.

DropdownMenuCheckboxItem / DropdownMenuRadioGroup / DropdownMenuRadioItem

Stateful items with check and radio indicators.

PropTypeDefaultDescription
checked / defaultChecked / onCheckedChangeboolean / boolean / (checked) => voidCheckbox item state (checkbox items).

DropdownMenuLabel / DropdownMenuSeparator / DropdownMenuGroup / DropdownMenuSub*

Engraved group labels, machined separators, grouping, and submenu parts.

Keyboard

KeysAction
ArrowDownArrowUpMoves the highlight through the menu.
EnterActivates the highlighted item.
ArrowRightOpens a submenu.
EscCloses the menu and returns focus.

Source

View source — dropdown-menu.tsx
dropdown-menu.tsx
"use client";

import { Menu as BaseMenu } from "@base-ui/react/menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";

const itemClassName =
  "grid cursor-pointer grid-cols-[1rem_1fr_auto] items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-well data-highlighted:shadow-deboss-1 data-[variant=destructive]:text-danger";

const popupClassName =
  "min-w-44 origin-(--transform-origin) rounded-md bg-surface-2 p-1 text-ink shadow-float-1 transition-[opacity,scale] duration-(--duration-press) ease-(--ease-press) data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0";

/**
 * A floating menu of actions. The highlighted item momentarily presses into
 * the surface; destructive items carry danger ink.
 *
 * @example
 * <DropdownMenu>
 *   <DropdownMenuTrigger render={<Button>Channel</Button>} />
 *   <DropdownMenuContent>
 *     <DropdownMenuItem onClick={duplicate}>Duplicate</DropdownMenuItem>
 *     <DropdownMenuItem variant="destructive">Delete</DropdownMenuItem>
 *   </DropdownMenuContent>
 * </DropdownMenu>
 */
function DropdownMenu(props: React.ComponentProps<typeof BaseMenu.Root>) {
  return <BaseMenu.Root {...props} />;
}

function DropdownMenuTrigger(
  props: React.ComponentProps<typeof BaseMenu.Trigger>,
) {
  return <BaseMenu.Trigger data-slot="dropdown-menu-trigger" {...props} />;
}

function DropdownMenuContent({
  className,
  sideOffset = 6,
  ...props
}: React.ComponentProps<typeof BaseMenu.Popup> & { sideOffset?: number }) {
  return (
    <BaseMenu.Portal>
      <BaseMenu.Positioner sideOffset={sideOffset} className="z-50">
        <BaseMenu.Popup
          data-slot="dropdown-menu-content"
          className={cn(popupClassName, className)}
          {...props}
        />
      </BaseMenu.Positioner>
    </BaseMenu.Portal>
  );
}

function DropdownMenuItem({
  className,
  variant,
  ...props
}: React.ComponentProps<typeof BaseMenu.Item> & {
  /** Destructive items carry danger ink. */
  variant?: "default" | "destructive";
}) {
  return (
    <BaseMenu.Item
      data-slot="dropdown-menu-item"
      data-variant={variant ?? "default"}
      className={cn(itemClassName, "grid-cols-1", className)}
      {...props}
    />
  );
}

function DropdownMenuCheckboxItem({
  className,
  children,
  ...props
}: React.ComponentProps<typeof BaseMenu.CheckboxItem>) {
  return (
    <BaseMenu.CheckboxItem
      data-slot="dropdown-menu-checkbox-item"
      className={cn(itemClassName, className)}
      {...props}
    >
      <BaseMenu.CheckboxItemIndicator className="col-start-1 flex">
        <Check className="size-3.5" aria-hidden />
      </BaseMenu.CheckboxItemIndicator>
      <span className="col-start-2">{children}</span>
    </BaseMenu.CheckboxItem>
  );
}

function DropdownMenuRadioGroup(
  props: React.ComponentProps<typeof BaseMenu.RadioGroup>,
) {
  return (
    <BaseMenu.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />
  );
}

function DropdownMenuRadioItem({
  className,
  children,
  ...props
}: React.ComponentProps<typeof BaseMenu.RadioItem>) {
  return (
    <BaseMenu.RadioItem
      data-slot="dropdown-menu-radio-item"
      className={cn(itemClassName, className)}
      {...props}
    >
      <BaseMenu.RadioItemIndicator className="col-start-1 flex">
        <Circle className="size-2 fill-current" aria-hidden />
      </BaseMenu.RadioItemIndicator>
      <span className="col-start-2">{children}</span>
    </BaseMenu.RadioItem>
  );
}

function DropdownMenuGroup(props: React.ComponentProps<typeof BaseMenu.Group>) {
  return <BaseMenu.Group data-slot="dropdown-menu-group" {...props} />;
}

function DropdownMenuLabel({
  className,
  ...props
}: React.ComponentProps<typeof BaseMenu.GroupLabel>) {
  return (
    <BaseMenu.GroupLabel
      data-slot="dropdown-menu-label"
      className={cn(
        "tracking-label px-2 py-1.5 font-mono text-label text-ink-faint uppercase text-engraved",
        className,
      )}
      {...props}
    />
  );
}

function DropdownMenuSeparator({
  className,
  ...props
}: React.ComponentProps<typeof BaseMenu.Separator>) {
  return (
    <BaseMenu.Separator
      data-slot="dropdown-menu-separator"
      className={cn("-mx-1 my-1 h-px bg-edge-line", className)}
      {...props}
    />
  );
}

function DropdownMenuSub(
  props: React.ComponentProps<typeof BaseMenu.SubmenuRoot>,
) {
  return <BaseMenu.SubmenuRoot {...props} />;
}

function DropdownMenuSubTrigger({
  className,
  children,
  ...props
}: React.ComponentProps<typeof BaseMenu.SubmenuTrigger>) {
  return (
    <BaseMenu.SubmenuTrigger
      data-slot="dropdown-menu-sub-trigger"
      className={cn(itemClassName, "grid-cols-[1fr_auto]", className)}
      {...props}
    >
      <span>{children}</span>
      <ChevronRight className="size-3.5 text-ink-faint" aria-hidden />
    </BaseMenu.SubmenuTrigger>
  );
}

function DropdownMenuSubContent({
  className,
  ...props
}: React.ComponentProps<typeof BaseMenu.Popup>) {
  return (
    <BaseMenu.Portal>
      <BaseMenu.Positioner className="z-50">
        <BaseMenu.Popup
          data-slot="dropdown-menu-sub-content"
          className={cn(popupClassName, className)}
          {...props}
        />
      </BaseMenu.Positioner>
    </BaseMenu.Portal>
  );
}

export {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuCheckboxItem,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuGroup,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuSub,
  DropdownMenuSubTrigger,
  DropdownMenuSubContent,
};