AniUI

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-menu

Requires @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
ContextMenu

Root wrapper — manages open/close state

ContextMenuTrigger

Long-press target that opens the menu

ContextMenuContent

Menu panel with items

ContextMenuItem

Individual menu action

ContextMenuSeparator

Visual divider between items

Props#

ContextMenu#

PropTypeDefault
open
boolean

Controlled open state

onOpenChange
(open: boolean) => void

Called when state changes

children
React.ReactNode

ContextMenuItem#

PropTypeDefault
destructive
boolean
false

Renders in destructive color

className
string
children
React.ReactNode

Accessibility#

  • Uses @rn-primitives/context-menu for proper menu semantics
  • accessibilityRole="menuitem" on each item
  • accessibilityHint="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)} />;
}