AniUI

Dropdown Menu

A context menu with items, separators, and destructive actions. Measures the trigger position and renders the menu nearby using a Modal overlay.

Web preview — components render natively on iOS & Android
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu";
<DropdownMenu>
  <DropdownMenuTrigger>
    <Button variant="outline">Open Menu</Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent>
    <DropdownMenuItem onPress={() => {}}>Edit</DropdownMenuItem>
    <DropdownMenuItem onPress={() => {}}>Duplicate</DropdownMenuItem>
    <DropdownMenuItem onPress={() => {}}>Delete</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>

Installation#

npx @aniui/cli add dropdown-menu

Positioning#

Pass side and align to DropdownMenuContent to control positioning. Collision detection automatically flips the menu if there isn't enough space.

Web preview — components render natively on iOS & Android
// Menu appears above the trigger, aligned to the right edge
<DropdownMenu>
  <DropdownMenuTrigger>
    <Button variant="outline">More</Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent side="top" align="end">
    <DropdownMenuItem onPress={() => {}}>Profile</DropdownMenuItem>
    <DropdownMenuItem onPress={() => {}}>Settings</DropdownMenuItem>
    <DropdownMenuSeparator />
    <DropdownMenuItem destructive onPress={() => {}}>Log out</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>

With Separator & Destructive#

Use DropdownMenuSeparator to divide groups and the destructive prop for danger actions.

Web preview — components render natively on iOS & Android
<DropdownMenu>
  <DropdownMenuTrigger><Button>Options</Button></DropdownMenuTrigger>
  <DropdownMenuContent>
    <DropdownMenuItem onPress={() => {}}>Edit</DropdownMenuItem>
    <DropdownMenuItem onPress={() => {}}>Share</DropdownMenuItem>
    <DropdownMenuSeparator />
    <DropdownMenuItem destructive onPress={() => {}}>Delete</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>

Props#

ComponentPropTypeDefault
DropdownMenu
children
ReactNode
DropdownMenu
open
boolean
DropdownMenu
onOpenChange
(open: boolean) => void
DropdownMenuContent
side
"top" | "bottom" | "left" | "right"
"bottom"
DropdownMenuContent
sideOffset
number
4
DropdownMenuContent
align
"start" | "center" | "end"
"start"
DropdownMenuContent
className
string
DropdownMenuItem
destructive
boolean
false
DropdownMenuItem
onPress
() => void

Accessibility#

  • Uses @rn-primitives/dropdown-menu for menu semantics
  • accessibilityRole="menuitem" on each item
  • BackHandler dismisses on Android
  • Collision detection for screen edge positioning
  • Requires <PortalHost /> at app root

Source#

components/ui/dropdown-menu.tsx
import React from "react";
import { View, Text, Pressable } from "react-native";
import * as DropdownMenuPrimitive from "@rn-primitives/dropdown-menu";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import { cn } from "@/lib/utils";

export interface DropdownMenuProps {
  children: React.ReactNode;
  open?: boolean;
  onOpenChange?: (open: boolean) => void;
}

export function DropdownMenu({ children, open, onOpenChange }: DropdownMenuProps) {
  return <DropdownMenuPrimitive.Root open={open} onOpenChange={onOpenChange}>{children}</DropdownMenuPrimitive.Root>;
}

export function DropdownMenuTrigger({ className, children, ...props }: React.ComponentPropsWithoutRef<typeof Pressable> & { className?: string; children: React.ReactNode }) {
  return (
    <DropdownMenuPrimitive.Trigger asChild>
      <Pressable className={cn("min-h-12 min-w-12", className)} accessible={true} accessibilityRole="button" {...props}>
        {children}
      </Pressable>
    </DropdownMenuPrimitive.Trigger>
  );
}

export interface DropdownMenuContentProps extends React.ComponentPropsWithoutRef<typeof View> {
  className?: string;
  children?: React.ReactNode;
  side?: "top" | "bottom" | "left" | "right";
  sideOffset?: number;
  align?: "start" | "center" | "end";
}

export function DropdownMenuContent({ className, children, side = "bottom", sideOffset = 4, align = "start", ...props }: DropdownMenuContentProps) {
  return (
    <DropdownMenuPrimitive.Portal>
      <DropdownMenuPrimitive.Overlay className="absolute inset-0" />
      <DropdownMenuPrimitive.Content side={side} sideOffset={sideOffset} align={align} avoidCollisions>
        <Animated.View entering={FadeIn.duration(150)} exiting={FadeOut.duration(100)}>
          <View className={cn("min-w-[180px] rounded-lg border border-border bg-card p-1 shadow-lg", className)} {...props}>
            {children}
          </View>
        </Animated.View>
      </DropdownMenuPrimitive.Content>
    </DropdownMenuPrimitive.Portal>
  );
}

export interface DropdownMenuItemProps extends React.ComponentPropsWithoutRef<typeof Pressable> {
  className?: string;
  children: React.ReactNode;
  destructive?: boolean;
}

export function DropdownMenuItem({ className, children, destructive, ...props }: DropdownMenuItemProps) {
  return (
    <DropdownMenuPrimitive.Item asChild>
      <Pressable className={cn("flex-row items-center rounded-md px-3 py-2.5 min-h-11", className)} accessible={true} accessibilityRole="menuitem" {...props}>
        {typeof children === "string" ? (
          <Text className={cn("text-sm", destructive ? "text-destructive" : "text-foreground")}>{children}</Text>
        ) : children}
      </Pressable>
    </DropdownMenuPrimitive.Item>
  );
}

export function DropdownMenuSeparator({ className }: { className?: string }) {
  return <DropdownMenuPrimitive.Separator className={cn("my-1 h-px bg-border", className)} />;
}