For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Dropdown Menu

A popover menu of grouped actions. Unlike Select, it does not pick a value into its trigger.

Installation

$ pnpm dlx shadcn@latest add @cobalt/dropdown-menu

Model picker

A radio group. The selected row keeps the hover fill and shows a radio glyph at the end of the row with indicator="radio".

Team menu

A header and a footer sit next to the groups as plain siblings. Rows can carry a trailing badge, and DropdownMenuSeparator bleeds through the panel padding.

Account menu

Row content is free-form, so rows can hold avatars instead of icons, and the panel can end with a divider and a pair of buttons.

Options

  • <DropdownMenuContent size="sm"> tighter menu: 12px radius and 6px padding instead of 16px and 10px.
  • <DropdownMenuRadioItem indicator="radio"> shows a radio glyph instead of a check.

Source

"use client";

import { Menu as MenuPrimitive } from "@base-ui/react/menu";
import { RiArrowRightSLine, RiCheckLine } from "@remixicon/react";
import { cn } from "cn";
import * as React from "react";

function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
  return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}

function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
  return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
}

function DropdownMenuTrigger({
  className,
  surface = "none",
  ...props
}: MenuPrimitive.Trigger.Props & {
  /** `tile` is a filled 12px row (account switchers): hairline on hover, hairline and a darker fill while open. */
  surface?: "none" | "tile";
}) {
  return (
    <MenuPrimitive.Trigger
      data-slot="dropdown-menu-trigger"
      className={
        surface === "tile"
          ? cn(
              "overflow-hidden rounded-[12px] border border-transparent bg-muted p-2.25 transition-colors duration-150 hover:border-[var(--border-hover,var(--border))] hover:bg-[var(--surface-strong,var(--accent))] data-popup-open:border-border data-popup-open:bg-[var(--surface-strong,var(--accent))] motion-reduce:transition-none",
              className as string | undefined
            )
          : className
      }
      {...props}
    />
  );
}

function DropdownMenuContent({
  align = "start",
  alignOffset = 0,
  side = "bottom",
  sideOffset = 4,
  collisionPadding = 8,
  size = "default",
  className,
  ...props
}: MenuPrimitive.Popup.Props &
  Pick<
    MenuPrimitive.Positioner.Props,
    "align" | "alignOffset" | "side" | "sideOffset" | "collisionPadding"
  > & { size?: "default" | "sm" }) {
  return (
    <MenuPrimitive.Portal>
      <MenuPrimitive.Positioner
        collisionPadding={collisionPadding}
        className="isolate z-50 outline-none"
        align={align}
        alignOffset={alignOffset}
        side={side}
        sideOffset={sideOffset}
      >
        <MenuPrimitive.Popup
          data-slot="dropdown-menu-content"
          data-size={size}
          className={cn(
            "group/dropdown-menu-content flex max-h-(--available-height) w-[266px] max-w-[calc(100vw-32px)] origin-(--transform-origin) flex-col gap-1 overflow-x-hidden overflow-y-auto rounded-[16px] border border-[var(--border-subtle,var(--border))] bg-[var(--surface-raised,var(--popover))] dark:border-border p-2.5 text-popover-foreground shadow-[shadow:var(--shadow-dropdown,0_1px_1px_0_rgb(0_0_0/0.04),0_4px_4px_0_rgb(0_0_0/0.02))] outline-none transition-[opacity,scale,filter] duration-150 ease-out data-starting-style:scale-95 data-starting-style:opacity-0 data-starting-style:blur-[2px] data-ending-style:scale-95 data-ending-style:opacity-0 data-ending-style:blur-[2px] motion-reduce:transition-none motion-reduce:data-starting-style:scale-100 motion-reduce:data-starting-style:opacity-100 motion-reduce:data-starting-style:blur-none motion-reduce:data-ending-style:scale-100 motion-reduce:data-ending-style:opacity-100 motion-reduce:data-ending-style:blur-none",
            size === "sm" && "rounded-[12px] p-1.5",
            className
          )}
          {...props}
        />
      </MenuPrimitive.Positioner>
    </MenuPrimitive.Portal>
  );
}

function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
  return (
    <MenuPrimitive.Group
      data-slot="dropdown-menu-group"
      className="flex w-full flex-col gap-1"
      {...props}
    />
  );
}

function DropdownMenuLabel({
  className,
  inset,
  ...props
}: MenuPrimitive.GroupLabel.Props & {
  inset?: boolean;
}) {
  return (
    <MenuPrimitive.GroupLabel
      data-slot="dropdown-menu-label"
      data-inset={inset}
      className={cn(
        "px-2 pt-1 pb-0.5 text-sm leading-5 font-medium text-muted-foreground data-inset:pl-8",
        className
      )}
      {...props}
    />
  );
}

function DropdownMenuItem({
  className,
  inset,
  variant = "default",
  ...props
}: MenuPrimitive.Item.Props & {
  inset?: boolean;
  variant?: "default" | "destructive";
}) {
  return (
    <MenuPrimitive.Item
      data-slot="dropdown-menu-item"
      data-inset={inset}
      data-variant={variant}
      className={cn(
        "group/dropdown-menu-item relative flex w-full cursor-pointer items-center gap-2 rounded-[10px] p-2 text-start text-sm leading-5 font-medium outline-none select-none transition-colors group-data-ending-style/dropdown-menu-content:delay-150 motion-reduce:transition-none data-highlighted:bg-[var(--surface-hover,var(--accent))] data-inset:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5 [&_svg:not([class*='text-'])]:text-[color:var(--icon-foreground,var(--muted-foreground))] data-[variant=destructive]:[&_svg:not([class*='text-'])]:text-destructive",
        className
      )}
      {...props}
    />
  );
}

function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
  return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />;
}

function DropdownMenuSubTrigger({
  className,
  inset,
  children,
  ...props
}: MenuPrimitive.SubmenuTrigger.Props & {
  inset?: boolean;
}) {
  return (
    <MenuPrimitive.SubmenuTrigger
      data-slot="dropdown-menu-sub-trigger"
      data-inset={inset}
      className={cn(
        "flex w-full cursor-pointer items-center gap-2 rounded-[10px] p-2 text-start text-sm leading-5 font-medium outline-none select-none transition-colors group-data-ending-style/dropdown-menu-content:delay-150 motion-reduce:transition-none data-highlighted:bg-[var(--surface-hover,var(--accent))] data-popup-open:bg-[var(--surface-hover,var(--accent))] data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5 [&_svg:not([class*='text-'])]:text-[color:var(--icon-foreground,var(--muted-foreground))] data-[variant=destructive]:[&_svg:not([class*='text-'])]:text-destructive",
        className
      )}
      {...props}
    >
      {children}
      <RiArrowRightSLine className="ml-auto" />
    </MenuPrimitive.SubmenuTrigger>
  );
}

function DropdownMenuSubContent({
  align = "start",
  alignOffset = -3,
  side = "right",
  sideOffset = 0,
  className,
  ...props
}: React.ComponentProps<typeof DropdownMenuContent>) {
  return (
    <DropdownMenuContent
      data-slot="dropdown-menu-sub-content"
      className={cn("w-[266px]", className)}
      align={align}
      alignOffset={alignOffset}
      side={side}
      sideOffset={sideOffset}
      {...props}
    />
  );
}

function DropdownMenuCheckboxItem({
  className,
  children,
  checked,
  inset,
  ...props
}: MenuPrimitive.CheckboxItem.Props & {
  inset?: boolean;
}) {
  return (
    <MenuPrimitive.CheckboxItem
      data-slot="dropdown-menu-checkbox-item"
      data-inset={inset}
      className={cn(
        "relative flex w-full cursor-pointer items-center gap-2 rounded-[10px] p-2 pr-8 text-start text-sm leading-5 font-medium outline-none select-none transition-colors group-data-ending-style/dropdown-menu-content:delay-150 motion-reduce:transition-none isolate before:absolute before:inset-0 before:-z-10 before:rounded-[inherit] data-checked:transition-none data-checked:not-data-highlighted:before:bg-[var(--surface-hover,var(--accent))] data-highlighted:bg-[var(--surface-hover,var(--accent))] data-inset:pl-8 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      checked={checked}
      {...props}
    >
      <span
        className="pointer-events-none absolute right-2 flex items-center justify-center"
        data-slot="dropdown-menu-checkbox-item-indicator"
      >
        <MenuPrimitive.CheckboxItemIndicator>
          <RiCheckLine />
        </MenuPrimitive.CheckboxItemIndicator>
      </span>
      {children}
    </MenuPrimitive.CheckboxItem>
  );
}

function DropdownMenuRadioGroup({
  className,
  ...props
}: MenuPrimitive.RadioGroup.Props) {
  return (
    <MenuPrimitive.RadioGroup
      data-slot="dropdown-menu-radio-group"
      className={cn("flex w-full flex-col gap-1", className)}
      {...props}
    />
  );
}

function DropdownMenuRadioItem({
  className,
  children,
  inset,
  indicator = "check",
  ...props
}: MenuPrimitive.RadioItem.Props & {
  inset?: boolean;
  indicator?: "check" | "radio";
}) {
  return (
    <MenuPrimitive.RadioItem
      data-slot="dropdown-menu-radio-item"
      data-inset={inset}
      className={cn(
        "group/dropdown-menu-radio-item relative flex w-full cursor-pointer items-center gap-2 rounded-[10px] p-2 pr-8 text-start text-sm leading-5 font-medium outline-none select-none transition-colors group-data-ending-style/dropdown-menu-content:delay-150 motion-reduce:transition-none isolate before:absolute before:inset-0 before:-z-10 before:rounded-[inherit] data-checked:transition-none data-checked:not-data-highlighted:before:bg-[var(--surface-hover,var(--accent))] data-highlighted:bg-[var(--surface-hover,var(--accent))] data-inset:pl-8 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    >
      {indicator === "radio" ? (
        <span
          className="pointer-events-none absolute right-2 flex size-4 items-center justify-center rounded-full border border-[var(--border-hover,var(--border))] bg-popover transition-colors group-data-checked/dropdown-menu-radio-item:border-primary group-data-checked/dropdown-menu-radio-item:bg-primary motion-reduce:transition-none"
          data-slot="dropdown-menu-radio-item-indicator"
        >
          <MenuPrimitive.RadioItemIndicator className="size-1.5 rounded-full bg-primary-foreground" />
        </span>
      ) : (
        <span
          className="pointer-events-none absolute right-2 flex items-center justify-center"
          data-slot="dropdown-menu-radio-item-indicator"
        >
          <MenuPrimitive.RadioItemIndicator>
            <RiCheckLine />
          </MenuPrimitive.RadioItemIndicator>
        </span>
      )}
      {children}
    </MenuPrimitive.RadioItem>
  );
}

function DropdownMenuSeparator({
  className,
  ...props
}: MenuPrimitive.Separator.Props) {
  return (
    <MenuPrimitive.Separator
      data-slot="dropdown-menu-separator"
      className={cn(
        "-mx-2.5 my-1.5 h-px shrink-0 bg-[var(--border-subtle,var(--border))] dark:bg-border",
        className
      )}
      {...props}
    />
  );
}

function DropdownMenuShortcut({
  className,
  ...props
}: React.ComponentProps<"span">) {
  return (
    <span
      data-slot="dropdown-menu-shortcut"
      className={cn(
        "ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
        className
      )}
      {...props}
    />
  );
}

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

Radio and checkbox items

DropdownMenuRadioItem and DropdownMenuCheckboxItem keep the menu open after a pick. This is the Base UI default, so you can change several options in one visit. For a single choice that should close the menu, pass closeOnClick to the item.