Menu

PreviousNext

A list of actions in a dropdown, enhanced with keyboard navigation.

Docs
animate-uiui

Preview

Loading preview…
registry/components/base/menu/index.tsx
import * as React from 'react';

import {
  Menu as MenuPrimitive,
  MenuTrigger as MenuTriggerPrimitive,
  MenuPortal as MenuPortalPrimitive,
  MenuPopup as MenuPopupPrimitive,
  MenuPositioner as MenuPositionerPrimitive,
  MenuGroup as MenuGroupPrimitive,
  MenuGroupLabel as MenuGroupLabelPrimitive,
  MenuArrow as MenuArrowPrimitive,
  MenuItem as MenuItemPrimitive,
  MenuCheckboxItem as MenuCheckboxItemPrimitive,
  MenuCheckboxItemIndicator as MenuCheckboxItemIndicatorPrimitive,
  MenuRadioGroup as MenuRadioGroupPrimitive,
  MenuRadioItem as MenuRadioItemPrimitive,
  MenuRadioItemIndicator as MenuRadioItemIndicatorPrimitive,
  MenuHighlightItem as MenuHighlightItemPrimitive,
  MenuHighlight as MenuHighlightPrimitive,
  MenuSeparator as MenuSeparatorPrimitive,
  MenuShortcut as MenuShortcutPrimitive,
  MenuSubmenu as MenuSubmenuPrimitive,
  MenuSubmenuTrigger as MenuSubmenuTriggerPrimitive,
  type MenuProps as MenuPrimitiveProps,
  type MenuTriggerProps as MenuTriggerPrimitiveProps,
  type MenuPortalProps as MenuPortalPrimitiveProps,
  type MenuPopupProps as MenuPopupPrimitiveProps,
  type MenuPositionerProps as MenuPositionerPrimitiveProps,
  type MenuGroupProps as MenuGroupPrimitiveProps,
  type MenuGroupLabelProps as MenuGroupLabelPrimitiveProps,
  type MenuArrowProps as MenuArrowPrimitiveProps,
  type MenuItemProps as MenuItemPrimitiveProps,
  type MenuCheckboxItemProps as MenuCheckboxItemPrimitiveProps,
  type MenuRadioGroupProps as MenuRadioGroupPrimitiveProps,
  type MenuRadioItemProps as MenuRadioItemPrimitiveProps,
  type MenuSeparatorProps as MenuSeparatorPrimitiveProps,
  type MenuShortcutProps as MenuShortcutPrimitiveProps,
  type MenuSubmenuProps as MenuSubmenuPrimitiveProps,
  type MenuSubmenuTriggerProps as MenuSubmenuTriggerPrimitiveProps,
} from '@/components/animate-ui/primitives/base/menu';
import { cn } from '@/lib/utils';
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';

type MenuProps = MenuPrimitiveProps;

function Menu(props: MenuProps) {
  return <MenuPrimitive {...props} />;
}

type MenuTriggerProps = MenuTriggerPrimitiveProps;

function MenuTrigger(props: MenuTriggerProps) {
  return <MenuTriggerPrimitive {...props} />;
}

type MenuPortalProps = MenuPortalPrimitiveProps;

function MenuPortal(props: MenuPortalProps) {
  return <MenuPortalPrimitive {...props} />;
}

type MenuPanelProps = MenuPopupPrimitiveProps & MenuPositionerPrimitiveProps;

function MenuPanel({
  className,
  finalFocus,
  id,
  children,
  sideOffset = 4,
  transition = { duration: 0.2 },
  ...props
}: MenuPanelProps) {
  return (
    <MenuPortal>
      <MenuPositionerPrimitive
        className="z-50"
        sideOffset={sideOffset}
        {...props}
      >
        <MenuPopupPrimitive
          finalFocus={finalFocus}
          transition={transition}
          id={id}
          className={cn(
            'bg-popover text-popover-foreground max-h-(--available-height) min-w-[8rem] origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md outline-none',
            className,
          )}
        >
          <MenuHighlightPrimitive className="absolute inset-0 bg-accent z-0 rounded-sm">
            {children}
          </MenuHighlightPrimitive>
        </MenuPopupPrimitive>
      </MenuPositionerPrimitive>
    </MenuPortal>
  );
}

type MenuGroupProps = MenuGroupPrimitiveProps;

function MenuGroup(props: MenuGroupProps) {
  return <MenuGroupPrimitive {...props} />;
}

type MenuGroupLabelProps = MenuGroupLabelPrimitiveProps & {
  inset?: boolean;
};

function MenuGroupLabel({ className, inset, ...props }: MenuGroupLabelProps) {
  return (
    <MenuGroupLabelPrimitive
      data-inset={inset}
      className={cn(
        'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
        className,
      )}
      {...props}
    />
  );
}

type MenuItemProps = MenuItemPrimitiveProps & {
  inset?: boolean;
  variant?: 'default' | 'destructive';
};

function MenuItem({
  className,
  inset,
  variant = 'default',
  disabled,
  ...props
}: MenuItemProps) {
  return (
    <MenuHighlightItemPrimitive
      activeClassName={
        variant === 'destructive'
          ? 'bg-destructive/10 dark:bg-destructive/20'
          : ''
      }
      disabled={disabled}
    >
      <MenuItemPrimitive
        disabled={disabled}
        data-inset={inset}
        data-variant={variant}
        className={cn(
          "focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
          className,
        )}
        {...props}
      />
    </MenuHighlightItemPrimitive>
  );
}

type MenuCheckboxItemProps = MenuCheckboxItemPrimitiveProps;

function MenuCheckboxItem({
  className,
  children,
  checked,
  disabled,
  ...props
}: MenuCheckboxItemProps) {
  return (
    <MenuHighlightItemPrimitive disabled={disabled}>
      <MenuCheckboxItemPrimitive
        disabled={disabled}
        className={cn(
          "focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]: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 left-2 flex size-3.5 items-center justify-center">
          <MenuCheckboxItemIndicatorPrimitive
            initial={{ opacity: 0, scale: 0.5 }}
            animate={{ opacity: 1, scale: 1 }}
          >
            <CheckIcon className="size-4" />
          </MenuCheckboxItemIndicatorPrimitive>
        </span>
        {children}
      </MenuCheckboxItemPrimitive>
    </MenuHighlightItemPrimitive>
  );
}

type MenuRadioGroupProps = MenuRadioGroupPrimitiveProps;

function MenuRadioGroup(props: MenuRadioGroupProps) {
  return <MenuRadioGroupPrimitive {...props} />;
}

type MenuRadioItemProps = MenuRadioItemPrimitiveProps;

function MenuRadioItem({
  className,
  children,
  disabled,
  ...props
}: MenuRadioItemProps) {
  return (
    <MenuHighlightItemPrimitive disabled={disabled}>
      <MenuRadioItemPrimitive
        disabled={disabled}
        className={cn(
          "focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
          className,
        )}
        {...props}
      >
        <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
          <MenuRadioItemIndicatorPrimitive layoutId="dropdown-menu-item-indicator-radio">
            <CircleIcon className="size-2 fill-current" />
          </MenuRadioItemIndicatorPrimitive>
        </span>
        {children}
      </MenuRadioItemPrimitive>
    </MenuHighlightItemPrimitive>
  );
}

type MenuSeparatorProps = MenuSeparatorPrimitiveProps;

function MenuSeparator({ className, ...props }: MenuSeparatorProps) {
  return (
    <MenuSeparatorPrimitive
      className={cn('bg-border -mx-1 my-1 h-px', className)}
      {...props}
    />
  );
}

type MenuShortcutProps = MenuShortcutPrimitiveProps;

function MenuShortcut({ className, ...props }: MenuShortcutProps) {
  return (
    <MenuShortcutPrimitive
      className={cn(
        'text-muted-foreground ml-auto text-xs tracking-widest',
        className,
      )}
      {...props}
    />
  );
}

type MenuArrowProps = MenuArrowPrimitiveProps;

function MenuArrow(props: MenuArrowProps) {
  return <MenuArrowPrimitive {...props} />;
}

type MenuSubmenuProps = MenuSubmenuPrimitiveProps;

function MenuSubmenu(props: MenuSubmenuProps) {
  return <MenuSubmenuPrimitive {...props} />;
}

type MenuSubmenuTriggerProps = MenuSubmenuTriggerPrimitiveProps & {
  inset?: boolean;
  children?: React.ReactNode;
};

function MenuSubmenuTrigger({
  disabled,
  className,
  inset,
  children,
  ...props
}: MenuSubmenuTriggerProps) {
  return (
    <MenuHighlightItemPrimitive disabled={disabled}>
      <MenuSubmenuTriggerPrimitive
        disabled={disabled}
        data-inset={inset}
        className={cn(
          'focus:text-accent-foreground data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
          'aria-[expanded=true]:[&_[data-slot=chevron]]:rotate-90 [&_[data-slot=chevron]]:transition-transform [&_[data-slot=chevron]]:duration-300 [&_[data-slot=chevron]]:ease-in-out',
          className,
        )}
        {...props}
      >
        {children}
        <ChevronRightIcon data-slot="chevron" className="ml-auto size-4" />
      </MenuSubmenuTriggerPrimitive>
    </MenuHighlightItemPrimitive>
  );
}

type MenuSubmenuPanelProps = MenuPopupPrimitiveProps &
  MenuPositionerPrimitiveProps;

function MenuSubmenuPanel({
  className,
  finalFocus,
  id,
  children,
  sideOffset = 4,
  transition = { duration: 0.2 },
  ...props
}: MenuSubmenuPanelProps) {
  return (
    <MenuPortal>
      <MenuPositionerPrimitive
        className="z-50"
        sideOffset={sideOffset}
        {...props}
      >
        <MenuPopupPrimitive
          finalFocus={finalFocus}
          transition={transition}
          id={id}
          className={cn(
            'bg-popover text-popover-foreground max-h-(--available-height) min-w-[8rem] origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
            className,
          )}
        >
          {children}
        </MenuPopupPrimitive>
      </MenuPositionerPrimitive>
    </MenuPortal>
  );
}

export {
  Menu,
  MenuTrigger,
  MenuPortal,
  MenuPanel,
  MenuGroup,
  MenuGroupLabel,
  MenuItem,
  MenuCheckboxItem,
  MenuRadioGroup,
  MenuRadioItem,
  MenuSeparator,
  MenuShortcut,
  MenuArrow,
  MenuSubmenu,
  MenuSubmenuTrigger,
  MenuSubmenuPanel,
  type MenuProps,
  type MenuTriggerProps,
  type MenuPortalProps,
  type MenuPanelProps,
  type MenuGroupProps,
  type MenuGroupLabelProps,
  type MenuItemProps,
  type MenuCheckboxItemProps,
  type MenuRadioGroupProps,
  type MenuRadioItemProps,
  type MenuSeparatorProps,
  type MenuShortcutProps,
  type MenuArrowProps,
  type MenuSubmenuProps,
  type MenuSubmenuTriggerProps,
  type MenuSubmenuPanelProps,
};

Installation

npx shadcn@latest add @animate-ui/components-base-menu

Usage

import { ComponentsBaseMenu } from "@/components/ui/components-base-menu"
<ComponentsBaseMenu />