Context Menu
A long-press context menu with items, separators, and destructive actions. Built on @rn-primitives/context-menu.
Web preview — components render natively on iOS & Android
import {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
} from "@/components/ui/context-menu";
<ContextMenu>
<ContextMenuTrigger>
<View className="h-40 w-full items-center justify-center rounded-lg border border-dashed border-border">
<Text>Long press me</Text>
</View>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onPress={() => {}}>Edit</ContextMenuItem>
<ContextMenuItem onPress={() => {}}>Duplicate</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem destructive onPress={() => {}}>Delete</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>Installation#
npx @aniui/cli add context-menuRequires @rn-primitives/context-menu and @rn-primitives/portal. Add <PortalHost /> to your root layout.
Usage#
app/index.tsx
import {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
} from "@/components/ui/context-menu";
<ContextMenu>
<ContextMenuTrigger>
<View className="h-40 w-full items-center justify-center rounded-lg border border-dashed border-border">
<Text>Long press me</Text>
</View>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onPress={() => {}}>Edit</ContextMenuItem>
<ContextMenuItem onPress={() => {}}>Duplicate</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem destructive onPress={() => {}}>Delete</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>Components#
ComponentDescription
ContextMenuRoot wrapper — manages open/close state
ContextMenuTriggerLong-press target that opens the menu
ContextMenuContentMenu panel with items
ContextMenuItemIndividual menu action
ContextMenuSeparatorVisual divider between items
Props#
ContextMenu#
PropTypeDefault
openboolean—Controlled open state
onOpenChange(open: boolean) => void—Called when state changes
childrenReact.ReactNode—ContextMenuItem#
PropTypeDefault
destructivebooleanfalseRenders in destructive color
classNamestring—childrenReact.ReactNode—Accessibility#
- Uses
@rn-primitives/context-menufor proper menu semantics accessibilityRole="menuitem"on each itemaccessibilityHint="Long press for options"on trigger- Collision detection for screen edge positioning
- Requires
<PortalHost />at app root
Source#
components/ui/context-menu.tsx
import React from "react";
import { View, Text, Pressable } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import * as ContextMenuPrimitive from "@rn-primitives/context-menu";
import Animated from "react-native-reanimated";
import { entering, exiting } from "@/components/ui/animate";
import { cn } from "@/lib/utils";
export interface ContextMenuProps {
children: React.ReactNode;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
export function ContextMenu({ children, open, onOpenChange }: ContextMenuProps) {
return <ContextMenuPrimitive.Root open={open} onOpenChange={onOpenChange}>{children}</ContextMenuPrimitive.Root>;
}
export function ContextMenuTrigger({ className, children, ...props }: React.ComponentPropsWithoutRef<typeof Pressable> & { className?: string; children: React.ReactNode }) {
return (
<ContextMenuPrimitive.Trigger asChild>
<Pressable className={cn("", className)} accessible={true} accessibilityRole="button" accessibilityHint="Long press for options" {...props}>
{children}
</Pressable>
</ContextMenuPrimitive.Trigger>
);
}
export interface ContextMenuContentProps extends React.ComponentPropsWithoutRef<typeof View> {
className?: string;
children?: React.ReactNode;
}
export function ContextMenuContent({ className, children, ...props }: ContextMenuContentProps) {
const insets = useSafeAreaInsets();
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Overlay className="absolute inset-0" />
<ContextMenuPrimitive.Content avoidCollisions insets={insets}>
<Animated.View entering={entering.fadeIn} exiting={exiting.fadeOut}>
<View className={cn("min-w-[180px] rounded-lg border border-border bg-card p-1 shadow-lg", className)} {...props}>
{children}
</View>
</Animated.View>
</ContextMenuPrimitive.Content>
</ContextMenuPrimitive.Portal>
);
}
export interface ContextMenuItemProps extends React.ComponentPropsWithoutRef<typeof Pressable> {
className?: string;
children: React.ReactNode;
destructive?: boolean;
}
export function ContextMenuItem({ className, children, destructive, ...props }: ContextMenuItemProps) {
return (
<ContextMenuPrimitive.Item asChild>
<Pressable className={cn("flex-row items-center rounded-md px-3 py-2.5 min-h-12", className)} accessible={true} accessibilityRole="menuitem" {...props}>
{typeof children === "string" ? (
<Text className={cn("text-sm", destructive ? "text-destructive" : "text-foreground")}>{children}</Text>
) : children}
</Pressable>
</ContextMenuPrimitive.Item>
);
}
export function ContextMenuSeparator({ className }: { className?: string }) {
return <ContextMenuPrimitive.Separator className={cn("my-1 h-px bg-border", className)} />;
}

