Context Menu
Context Menu Primitive
Text Component
Terminal window
~/components/ui/context-menu.tsx
Demo
Shows a menu activated by either a right-click or a long-press.
Installation
npx @react-native-reusables/cli@latest add context-menu
Copy/paste the following code to ~/components/ui/context-menu.tsx
:
import * as ContextMenuPrimitive from '@rn-primitives/context-menu';import * as React from 'react';import { Platform, type StyleProp, StyleSheet, Text, type TextProps, View, type ViewStyle,} from 'react-native';import { Check } from '~/lib/icons/Check';import { ChevronDown } from '~/lib/icons/ChevronDown';import { ChevronRight } from '~/lib/icons/ChevronRight';import { ChevronUp } from '~/lib/icons/ChevronUp';import { cn } from '~/lib/utils';import { TextClassContext } from '~/components/ui/text';
const ContextMenu = ContextMenuPrimitive.Root;const ContextMenuTrigger = ContextMenuPrimitive.Trigger;const ContextMenuGroup = ContextMenuPrimitive.Group;const ContextMenuSub = ContextMenuPrimitive.Sub;const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
function ContextMenuSubTrigger({ className, inset, children, ...props}: ContextMenuPrimitive.SubTriggerProps & { ref?: React.RefObject<ContextMenuPrimitive.SubTriggerRef>; children?: React.ReactNode; inset?: boolean;}) { const { open } = ContextMenuPrimitive.useSubContext(); const Icon = Platform.OS === 'web' ? ChevronRight : open ? ChevronUp : ChevronDown; return ( <TextClassContext.Provider value={cn( 'select-none text-sm native:text-lg text-primary', open && 'native:text-accent-foreground' )} > <ContextMenuPrimitive.SubTrigger className={cn( 'flex flex-row web:cursor-default web:select-none items-center gap-2 web:focus:bg-accent active:bg-accent web:hover:bg-accent rounded-sm px-2 py-1.5 native:py-2 web:outline-none', open && 'bg-accent', inset && 'pl-8', className )} {...props} > {children} <Icon size={18} className='ml-auto text-foreground' /> </ContextMenuPrimitive.SubTrigger> </TextClassContext.Provider> );}
function ContextMenuSubContent({ className, ...props}: ContextMenuPrimitive.SubContentProps & { ref?: React.RefObject<ContextMenuPrimitive.SubContentRef>;}) { const { open } = ContextMenuPrimitive.useSubContext(); return ( <ContextMenuPrimitive.SubContent className={cn( 'z-50 min-w-[8rem] overflow-hidden rounded-md border mt-1 border-border bg-popover p-1 shadow-md shadow-foreground/5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', open ? 'web:animate-in web:fade-in-0 web:zoom-in-95' : 'web:animate-out web:fade-out-0 web:zoom-out', className )} {...props} /> );}
function ContextMenuContent({ className, overlayClassName, overlayStyle, portalHost, ...props}: ContextMenuPrimitive.ContentProps & { ref?: React.RefObject<ContextMenuPrimitive.ContentRef>; overlayStyle?: StyleProp<ViewStyle>; overlayClassName?: string; portalHost?: string;}) { const { open } = ContextMenuPrimitive.useRootContext(); return ( <ContextMenuPrimitive.Portal hostName={portalHost}> <ContextMenuPrimitive.Overlay style={ overlayStyle ? StyleSheet.flatten([ Platform.OS !== 'web' ? StyleSheet.absoluteFill : undefined, overlayStyle as typeof StyleSheet.absoluteFill, ]) : Platform.OS !== 'web' ? StyleSheet.absoluteFill : undefined } className={overlayClassName} > <ContextMenuPrimitive.Content className={cn( 'z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 shadow-md shadow-foreground/5 web:data-[side=bottom]:slide-in-from-top-2 web:data-[side=left]:slide-in-from-right-2 web:data-[side=right]:slide-in-from-left-2 web:data-[side=top]:slide-in-from-bottom-2', open ? 'web:animate-in web:fade-in-0 web:zoom-in-95' : 'web:animate-out web:fade-out-0 web:zoom-out-95', className )} {...props} /> </ContextMenuPrimitive.Overlay> </ContextMenuPrimitive.Portal> );}
function ContextMenuItem({ className, inset, ...props}: ContextMenuPrimitive.ItemProps & { ref?: React.RefObject<ContextMenuPrimitive.ItemRef>; className?: string; inset?: boolean;}) { return ( <TextClassContext.Provider value='select-none text-sm native:text-lg text-popover-foreground web:group-focus:text-accent-foreground'> <ContextMenuPrimitive.Item className={cn( 'relative flex flex-row web:cursor-default items-center gap-2 rounded-sm px-2 py-1.5 native:py-2 web:outline-none web:focus:bg-accent active:bg-accent web:hover:bg-accent group', inset && 'pl-8', props.disabled && 'opacity-50 web:pointer-events-none', className )} {...props} /> </TextClassContext.Provider> );}
function ContextMenuCheckboxItem({ className, children, ...props}: ContextMenuPrimitive.CheckboxItemProps & { ref?: React.RefObject<ContextMenuPrimitive.CheckboxItemRef>; children?: React.ReactNode;}) { return ( <ContextMenuPrimitive.CheckboxItem className={cn( 'relative flex flex-row web:cursor-default items-center web:group rounded-sm py-1.5 native:py-2 pl-8 pr-2 web:outline-none web:focus:bg-accent active:bg-accent', props.disabled && 'web:pointer-events-none opacity-50', className )} {...props} > <View className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'> <ContextMenuPrimitive.ItemIndicator> <Check size={14} strokeWidth={3} className='text-foreground' /> </ContextMenuPrimitive.ItemIndicator> </View> {children} </ContextMenuPrimitive.CheckboxItem> );}
function ContextMenuRadioItem({ className, children, ...props}: ContextMenuPrimitive.RadioItemProps & { ref?: React.RefObject<ContextMenuPrimitive.RadioItemRef>; children?: React.ReactNode;}) { return ( <ContextMenuPrimitive.RadioItem className={cn( 'relative flex flex-row web:cursor-default web:group items-center rounded-sm py-1.5 native:py-2 pl-8 pr-2 web:outline-none web:focus:bg-accent active:bg-accent', props.disabled && 'web:pointer-events-none opacity-50', className )} {...props} > <View className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'> <ContextMenuPrimitive.ItemIndicator> <View className='bg-foreground h-2 w-2 rounded-full' /> </ContextMenuPrimitive.ItemIndicator> </View> {children} </ContextMenuPrimitive.RadioItem> );}
function ContextMenuLabel({ className, inset, ...props}: ContextMenuPrimitive.LabelProps & { ref?: React.RefObject<ContextMenuPrimitive.LabelRef>; className?: string; inset?: boolean;}) { return ( <ContextMenuPrimitive.Label className={cn( 'px-2 py-1.5 text-sm native:text-base font-semibold text-foreground web:cursor-default', inset && 'pl-8', className )} {...props} /> );}
function ContextMenuSeparator({ className, ...props}: ContextMenuPrimitive.SeparatorProps & { ref?: React.RefObject<ContextMenuPrimitive.SeparatorRef>;}) { return ( <ContextMenuPrimitive.Separator className={cn('-mx-1 my-1 h-px bg-border', className)} {...props} /> );}
function ContextMenuShortcut({ className, ...props }: TextProps) { return ( <Text className={cn( 'ml-auto text-xs native:text-sm tracking-widest text-muted-foreground', className )} {...props} /> );}
export { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger,};
Usage
import * as React from 'react';import { Platform, View } from 'react-native';import Animated, { FadeIn } from 'react-native-reanimated';import { useSafeAreaInsets } from 'react-native-safe-area-context';import { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger,} from '~/components/ui/context-menu';import { Text } from '~/components/ui/text';
function Example() { const insets = useSafeAreaInsets(); const contentInsets = { top: insets.top, bottom: insets.bottom, left: 12, right: 12, }; const [checkboxValue, setCheckboxValue] = React.useState(false); const [subCheckboxValue, setSubCheckboxValue] = React.useState(false); const [radioValue, setRadioValue] = React.useState('pedro');
return ( <ContextMenu> <ContextMenuTrigger className='flex h-[150px] w-full max-w-[300px] mx-auto web:cursor-default items-center justify-center rounded-md border border-foreground border-dashed'> <Text className='text-foreground text-sm native:text-lg'> {Platform.OS === 'web' ? 'Right click here' : 'Long press here'} </Text> </ContextMenuTrigger>
<ContextMenuContent align='start' insets={contentInsets} className='w-64 native:w-72'> <ContextMenuItem inset> <Text>Back</Text> <ContextMenuShortcut>⌘[</ContextMenuShortcut> </ContextMenuItem> <ContextMenuItem inset disabled> <Text>Forward</Text> <ContextMenuShortcut>⌘]</ContextMenuShortcut> </ContextMenuItem> <ContextMenuItem inset> <Text>Reload</Text> <ContextMenuShortcut>⌘R</ContextMenuShortcut> </ContextMenuItem>
<ContextMenuSub> <ContextMenuSubTrigger inset> <Text>More Tools</Text> </ContextMenuSubTrigger> <ContextMenuSubContent className='web:w-48 native:mt-1'> <Animated.View entering={FadeIn.duration(200)}> <ContextMenuItem> <Text>Save Page As...</Text> <ContextMenuShortcut>⇧⌘S</ContextMenuShortcut> </ContextMenuItem> <ContextMenuItem> <Text>Create Shortcut...</Text> </ContextMenuItem>
<ContextMenuSeparator /> <ContextMenuItem> <Text>Developer Tools</Text> </ContextMenuItem> </Animated.View> </ContextMenuSubContent> </ContextMenuSub>
<ContextMenuSeparator /> <ContextMenuCheckboxItem checked={checkboxValue} onCheckedChange={setCheckboxValue} closeOnPress={false} > <Text>Show Bookmarks Bar</Text> <ContextMenuShortcut>⌘⇧B</ContextMenuShortcut> </ContextMenuCheckboxItem> <ContextMenuCheckboxItem checked={subCheckboxValue} onCheckedChange={setSubCheckboxValue} closeOnPress={false} > <Text>Show Full URLs</Text> </ContextMenuCheckboxItem> <ContextMenuSeparator /> <ContextMenuRadioGroup value={radioValue} onValueChange={setRadioValue}> <ContextMenuLabel inset>People</ContextMenuLabel> <ContextMenuSeparator /> <ContextMenuRadioItem value='pedro' closeOnPress={false}> <Text>Elmer Fudd</Text> </ContextMenuRadioItem> <ContextMenuRadioItem value='colm' closeOnPress={false}> <Text>Foghorn Leghorn</Text> </ContextMenuRadioItem> </ContextMenuRadioGroup> </ContextMenuContent> </ContextMenu> );}
Props
ContextMenu
Extends View
props
Prop | Type | Note |
---|---|---|
onOpenChange | (value: boolean) => void | |
asChild | boolean | (optional) |
relativeTo | ’longPress’ | ‘trigger’ | Native Only (optional) |
ContextMenuTrigger
Extends Pressable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
inset | boolean | (optional) |
ContextMenuPortal
Prop | Type | Note |
---|---|---|
children* | React.ReactNode | |
forceMount | true | undefined | (optional) |
hostName | string | Web Only (optional) |
container | HTMLElement | null | undefined | Web Only (optional) |
ContextMenuContent
Extends View
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
overlayStyle | StyleProp<ViewStyle> | (optional) |
overlayClassName | string | (optional) |
forceMount | true | undefined | (optional) |
alignOffset | number | (optional) |
insets | Insets | (optional) |
avoidCollisions | boolean | (optional) |
align | ’start’ | ‘center’ | ‘end’ | (optional) |
side | ’top’ | ‘bottom’ | (optional) |
sideOffset | number | (optional) |
disablePositioningStyle | boolean | Native Only (optional) |
loop | boolean | Web Only (optional) |
onCloseAutoFocus | (event: Event) => void | Web Only (optional) |
onEscapeKeyDown | (event: KeyboardEvent) => void | Web Only (optional) |
onPointerDownOutside | (event: PointerDownOutsideEvent) => void | Web Only (optional) |
onFocusOutside | (event: FocusOutsideEvent) => void | Web Only (optional) |
onInteractOutside | PointerDownOutsideEvent | FocusOutsideEvent | Web Only (optional) |
collisionBoundary | Element | null | Array<Element | null> | Web Only (optional) |
sticky | ’partial’ | ‘always’ | Web Only (optional) |
hideWhenDetached | boolean | Web Only (optional) |
ContextMenuGroup
Extends Text
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
ContextMenuLabel
Extends Text
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
ContextMenuItem
Extends Pressable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
textValue | boolean | (optional) |
closeOnPress | boolean | (optional) |
ContextMenuCheckboxItem
Extends Pressable
props
Prop | Type | Note |
---|---|---|
checked* | boolean | |
onCheckedChange* | (checked: boolean) => void | |
textValue* | string | |
asChild | boolean | (optional) |
closeOnPress | boolean | Native Only (optional) |
ContextMenuRadioGroup
Extends View
props
Prop | Type | Note |
---|---|---|
value* | boolean | |
onValueChange* | (value: string) => void | |
asChild | boolean | (optional) |
ContextMenuRadioItem
Extends Pressable
props
Prop | Type | Note |
---|---|---|
value* | boolean | |
onCheckedChange* | (value: boolean) => void | |
asChild | boolean | (optional) |
closeOnPress | boolean | Native Only (optional) |
ContextMenuSeparator
Extends View
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
decorative | boolean | (optional) |
ContextMenuSub
Extends View
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
defaultOpen | boolean | (optional) |
open | boolean | (optional) |
onOpenChange | (value: boolean) => void | (optional) |
ContextMenuSubTrigger
Extends Pressable
props
Prop | Type | Note |
---|---|---|
textValue | string | (optional) |
asChild | boolean | (optional) |
ContextMenuSubContent
Extends Pressable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
forceMount | true / | undefined |
ContextMenuShortcut
Extends Text
props