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;
const ContextMenuSubTrigger = React.forwardRef< ContextMenuPrimitive.SubTriggerRef, ContextMenuPrimitive.SubTriggerProps & { inset?: boolean; }>(({ className, inset, children, ...props }, ref) => { 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 ref={ref} 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> );});ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
const ContextMenuSubContent = React.forwardRef< ContextMenuPrimitive.SubContentRef, ContextMenuPrimitive.SubContentProps>(({ className, ...props }, ref) => { const { open } = ContextMenuPrimitive.useSubContext(); return ( <ContextMenuPrimitive.SubContent ref={ref} 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} /> );});ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
const ContextMenuContent = React.forwardRef< ContextMenuPrimitive.ContentRef, ContextMenuPrimitive.ContentProps & { overlayStyle?: StyleProp<ViewStyle>; overlayClassName?: string; portalHost?: string; }>(({ className, overlayClassName, overlayStyle, portalHost, ...props }, ref) => { const { open } = ContextMenuPrimitive.useRootContext(); return ( <ContextMenuPrimitive.Portal hostName={portalHost}> <ContextMenuPrimitive.Overlay style={ overlayStyle ? StyleSheet.flatten([ Platform.OS !== 'web' ? StyleSheet.absoluteFill : undefined, overlayStyle, ]) : Platform.OS !== 'web' ? StyleSheet.absoluteFill : undefined } className={overlayClassName} > <ContextMenuPrimitive.Content ref={ref} 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> );});ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
const ContextMenuItem = React.forwardRef< ContextMenuPrimitive.ItemRef, ContextMenuPrimitive.ItemProps & { inset?: boolean; }>(({ className, inset, ...props }, ref) => ( <TextClassContext.Provider value='select-none text-sm native:text-lg text-popover-foreground web:group-focus:text-accent-foreground'> <ContextMenuPrimitive.Item ref={ref} 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>));ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
const ContextMenuCheckboxItem = React.forwardRef< ContextMenuPrimitive.CheckboxItemRef, ContextMenuPrimitive.CheckboxItemProps>(({ className, children, ...props }, ref) => ( <ContextMenuPrimitive.CheckboxItem ref={ref} 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>));ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
const ContextMenuRadioItem = React.forwardRef< ContextMenuPrimitive.RadioItemRef, ContextMenuPrimitive.RadioItemProps>(({ className, children, ...props }, ref) => ( <ContextMenuPrimitive.RadioItem ref={ref} 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>));ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
const ContextMenuLabel = React.forwardRef< ContextMenuPrimitive.LabelRef, ContextMenuPrimitive.LabelProps & { inset?: boolean; }>(({ className, inset, ...props }, ref) => ( <ContextMenuPrimitive.Label ref={ref} className={cn( 'px-2 py-1.5 text-sm native:text-base font-semibold text-foreground web:cursor-default', inset && 'pl-8', className )} {...props} />));ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
const ContextMenuSeparator = React.forwardRef< ContextMenuPrimitive.SeparatorRef, ContextMenuPrimitive.SeparatorProps>(({ className, ...props }, ref) => ( <ContextMenuPrimitive.Separator ref={ref} className={cn('-mx-1 my-1 h-px bg-border', className)} {...props} />));ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
const ContextMenuShortcut = ({ className, ...props }: TextProps) => { return ( <Text className={cn( 'ml-auto text-xs native:text-sm tracking-widest text-muted-foreground', className )} {...props} /> );};ContextMenuShortcut.displayName = 'ContextMenuShortcut';
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