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-menuPositioning#
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
DropdownMenuchildrenReactNode—DropdownMenuopenboolean—DropdownMenuonOpenChange(open: boolean) => void—DropdownMenuContentside"top" | "bottom" | "left" | "right""bottom"DropdownMenuContentsideOffsetnumber4DropdownMenuContentalign"start" | "center" | "end""start"DropdownMenuContentclassNamestring—DropdownMenuItemdestructivebooleanfalseDropdownMenuItemonPress() => void—Accessibility#
- Uses
@rn-primitives/dropdown-menufor 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)} />;
}