Composite

DropMenu

DropMenu builds on popover primitives with menu semantics for profile menus, action lists, and command palettes. Pair with an avatar or icon trigger for app-shell headers.

Installation

npx shadcn add https://rck.vibeboxph.com//r/drop-menu.json

Live preview & controls

Acme Labs

Click the trigger to open the menu

Controls
"use client";

import { Avatar } from "@/registry/rck/ui/avatar";
import { Button } from "@/registry/rck/ui/button";
import {
  DropMenu,
  DropMenuContent,
  DropMenuItem,
  DropMenuLabel,
  DropMenuSeparator,
  DropMenuTrigger,
} from "@/registry/rck/ui/drop-menu";
import { Icon } from "@/registry/rck/ui/icon";

export default function DropMenuDemo() {
  return (
    <div className="flex w-full max-w-md items-center justify-between gap-4 rounded-xl border border-border bg-surface/80 px-4 py-3 shadow-sm">
      <div className="min-w-0">
        <p className="truncate text-sm font-semibold">Acme Labs</p>
        <p className="truncate text-xs text-muted-foreground">Click the trigger to open the menu</p>
      </div>
      <DropMenu>
        <DropMenuTrigger asChild>
          <button
            type="button"
            className="inline-flex items-center gap-2.5 rounded-full border border-border bg-background py-1 pr-2 pl-1 shadow-sm outline-none transition-colors hover:bg-surface-2 focus-visible:ring-2 focus-visible:ring-ring"
            aria-label="Open account menu"
          >
            <Avatar name="Ada Lovelace" className="size-8" />
            <span className="hidden min-w-0 flex-col items-start text-left sm:flex">
              <span className="max-w-[8rem] truncate text-sm font-semibold leading-tight">Ada Lovelace</span>
              <span className="text-[11px] leading-tight text-muted-foreground">Admin</span>
            </span>
            <Icon name="chevron-down" size="xs" className="text-muted-foreground" />
          </button>
        </DropMenuTrigger>
        <DropMenuContent className="w-56">
          <div className="px-2 py-2">
            <p className="truncate text-sm font-semibold">Ada Lovelace</p>
            <p className="truncate text-[11px] text-muted-foreground">ada@acmelabs.io</p>
          </div>
          <DropMenuSeparator />
          <DropMenuItem icon="plus">Create project</DropMenuItem>
          <DropMenuItem icon="settings">Settings</DropMenuItem>
          <DropMenuItem icon="external-link">Documentation</DropMenuItem>
          <DropMenuSeparator />
          <DropMenuItem icon="close" destructive>
            Sign out
          </DropMenuItem>
        </DropMenuContent>
      </DropMenu>
    </div>
  );
}

Anatomy

  • DropMenuThe root, holds open state.
  • DropMenuTriggerThe button or custom element that opens the menu.
  • DropMenuContentThe floating menu panel.
  • DropMenuItemA selectable action row; closes the menu on activate.
  • DropMenuCheckboxItemA menu row with a trailing check indicator for toggle state.
  • DropMenuGroupGroups related menu items with `role="group"`.
  • DropMenuLabel / DropMenuSeparatorNon-interactive section label and divider.

Props

PropTypeDefaultDescription
open / onOpenChangeboolean / (open: boolean) => voidControlled open state.
defaultOpenbooleanInitial open state when uncontrolled.
modalbooleanWhen true, limits interaction outside the menu like a dialog.
asChild (DropMenuTrigger)booleanMerge props onto the child element instead of rendering a button.
align / side (DropMenuContent)start | center | end / top | right | bottom | leftPlacement relative to the trigger.
sideOffset (DropMenuContent)number6Gap in pixels between trigger and menu.
inset (DropMenuItem / DropMenuLabel)booleanExtra left padding for nested menu rows.
icon (DropMenuItem)IconNameOptional leading icon from the curated registry.
destructive (DropMenuItem)booleanApplies destructive styling for irreversible actions.
onSelect (DropMenuItem)() => voidCallback fired when the item is activated.
checked (DropMenuCheckboxItem)booleanShows a trailing check when true.

Accessibility

  • Trigger exposes `aria-haspopup="menu"`; content renders as `role="menu"` with `menuitem` rows.
  • Items are native buttons for keyboard activation; the menu closes after a selection.
  • Use `DropMenuLabel` for section headings and keep destructive actions at the bottom separated by a `DropMenuSeparator`.

Source

registry/rck/ui/drop-menu.tsx
"use client";

import * as React from "react";
import * as PopoverPrimitive from "@/registry/rck/primitives/popover";
import { cn } from "@/lib/utils";
import { Icon, type IconName } from "@/registry/rck/ui/icon";

export const DropMenu = PopoverPrimitive.Root;

export const DropMenuTrigger = React.forwardRef<
  HTMLButtonElement,
  React.ButtonHTMLAttributes<HTMLButtonElement> & { asChild?: boolean }
>(({ asChild, ...props }, ref) => (
  <PopoverPrimitive.Trigger ref={ref} asChild={asChild} {...props} aria-haspopup="menu" />
));
DropMenuTrigger.displayName = "DropMenuTrigger";

export const DropMenuContent = React.forwardRef<
  HTMLDivElement,
  Omit<React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>, "placement"> & {
    side?: "top" | "right" | "bottom" | "left";
  }
>(({ className, align = "end", side = "bottom", sideOffset = 6, ...props }, ref) => (
  <PopoverPrimitive.Portal>
    <PopoverPrimitive.Content
      ref={ref}
      role="menu"
      aria-orientation="vertical"
      align={align}
      placement={side}
      sideOffset={sideOffset}
      className={cn(
        "border-border bg-popover text-popover-foreground z-50 min-w-[12rem] overflow-hidden rounded-lg border p-1 shadow-md outline-none",
        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
        "data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
        "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
        "data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
        className,
      )}
      {...props}
    />
  </PopoverPrimitive.Portal>
));
DropMenuContent.displayName = "DropMenuContent";

export function DropMenuLabel({
  className,
  inset = false,
  ...props
}: React.HTMLAttributes<HTMLDivElement> & { inset?: boolean }) {
  return (
    <div
      role="presentation"
      data-slot="drop-menu-label"
      className={cn(
        "text-muted-foreground px-2 py-1.5 text-[11px] font-semibold",
        inset && "pl-8",
        className,
      )}
      {...props}
    />
  );
}

export function DropMenuSeparator({
  className,
  ...props
}: React.HTMLAttributes<HTMLHRElement>) {
  return (
    <hr
      role="separator"
      aria-orientation="horizontal"
      data-slot="drop-menu-separator"
      className={cn("bg-border -mx-1 my-1 h-px border-0", className)}
      {...props}
    />
  );
}

export function DropMenuGroup({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      role="group"
      data-slot="drop-menu-group"
      className={cn("flex flex-col gap-0.5", className)}
      {...props}
    />
  );
}

export interface DropMenuItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  inset?: boolean;
  destructive?: boolean;
  icon?: IconName;
  /** Fires before the menu closes. */
  onSelect?: () => void;
}

export const DropMenuItem = React.forwardRef<HTMLButtonElement, DropMenuItemProps>(
  (
    {
      className,
      inset = false,
      destructive = false,
      icon,
      children,
      disabled,
      onSelect,
      onClick,
      ...props
    },
    ref,
  ) => {
    return (
      <PopoverPrimitive.Close asChild>
        <button
          ref={ref}
          type="button"
          role="menuitem"
          disabled={disabled}
          className={cn(
            "flex w-full cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-left text-[11px] font-medium transition-colors outline-none",
            "focus-visible:bg-surface-2 focus-visible:text-foreground",
            "disabled:pointer-events-none disabled:opacity-50",
            destructive
              ? "text-destructive hover:bg-destructive/10 focus-visible:bg-destructive/10"
              : "text-foreground hover:bg-surface-2",
            inset && "pl-8",
            className,
          )}
          onClick={(event) => {
            onClick?.(event);
            if (!event.defaultPrevented) onSelect?.();
          }}
          {...props}
        >
          {icon ? <Icon name={icon} size="xs" className="text-muted-foreground" /> : null}
          <span className="flex-1 truncate">{children}</span>
        </button>
      </PopoverPrimitive.Close>
    );
  },
);
DropMenuItem.displayName = "DropMenuItem";

export interface DropMenuCheckboxItemProps extends DropMenuItemProps {
  checked?: boolean;
}

export const DropMenuCheckboxItem = React.forwardRef<
  HTMLButtonElement,
  DropMenuCheckboxItemProps
>(({ className, checked, children, ...props }, ref) => {
  return (
    <DropMenuItem ref={ref} className={cn("relative pr-8", className)} {...props}>
      <span className="absolute right-2 flex size-3.5 items-center justify-center">
        {checked ? <Icon name="check" size="xs" className="text-primary" /> : null}
      </span>
      {children}
    </DropMenuItem>
  );
});
DropMenuCheckboxItem.displayName = "DropMenuCheckboxItem";

Related examples

Browse the full examples gallery