Menubar
Menubar Primitive
Text Component
Terminal window
~/components/ui/menubar.tsx
Demo
A menu that stays visible on the screen, often seen in desktop apps, offering easy access to a standard set of commands.
Installation
npx @react-native-reusables/cli@latest add menubar
Copy/paste the following code to ~/components/ui/menubar.tsx
:
import * as MenubarPrimitive from '@rn-primitives/menubar';import * as React from 'react';import { Platform, Text, type TextProps, View } 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 MenubarMenu = MenubarPrimitive.Menu;
const MenubarGroup = MenubarPrimitive.Group;
const MenubarPortal = MenubarPrimitive.Portal;
const MenubarSub = MenubarPrimitive.Sub;
const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
const Menubar = React.forwardRef<MenubarPrimitive.RootRef, MenubarPrimitive.RootProps>( ({ className, ...props }, ref) => ( <MenubarPrimitive.Root ref={ref} className={cn( 'flex flex-row h-10 native:h-12 items-center space-x-1 rounded-md border border-border bg-background p-1', className )} {...props} /> ));Menubar.displayName = MenubarPrimitive.Root.displayName;
const MenubarTrigger = React.forwardRef<MenubarPrimitive.TriggerRef, MenubarPrimitive.TriggerProps>( ({ className, ...props }, ref) => { const { value } = MenubarPrimitive.useRootContext(); const { value: itemValue } = MenubarPrimitive.useMenuContext();
return ( <MenubarPrimitive.Trigger ref={ref} className={cn( 'flex flex-row web:cursor-default web:select-none items-center rounded-sm px-3 py-1.5 text-sm native:h-10 native:px-5 native:py-0 font-medium web:outline-none web:focus:bg-accent active:bg-accent web:focus:text-accent-foreground', value === itemValue && 'bg-accent text-accent-foreground', className )} {...props} /> ); });MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
const MenubarSubTrigger = React.forwardRef< MenubarPrimitive.SubTriggerRef, MenubarPrimitive.SubTriggerProps & { inset?: boolean; }>(({ className, inset, children, ...props }, ref) => { const { open } = MenubarPrimitive.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' )} > <MenubarPrimitive.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' /> </MenubarPrimitive.SubTrigger> </TextClassContext.Provider> );});MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
const MenubarSubContent = React.forwardRef< MenubarPrimitive.SubContentRef, MenubarPrimitive.SubContentProps>(({ className, ...props }, ref) => { const { open } = MenubarPrimitive.useSubContext(); return ( <MenubarPrimitive.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} /> );});MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
const MenubarContent = React.forwardRef< MenubarPrimitive.ContentRef, MenubarPrimitive.ContentProps & { portalHost?: string }>(({ className, portalHost, ...props }, ref) => { const { value } = MenubarPrimitive.useRootContext(); const { value: itemValue } = MenubarPrimitive.useMenuContext(); return ( <MenubarPrimitive.Portal hostName={portalHost}> <MenubarPrimitive.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', value === itemValue ? 'web:animate-in web:fade-in-0 web:zoom-in-95' : 'web:animate-out web:fade-out-0 web:zoom-out-95', className )} {...props} /> </MenubarPrimitive.Portal> );});MenubarContent.displayName = MenubarPrimitive.Content.displayName;
const MenubarItem = React.forwardRef< MenubarPrimitive.ItemRef, MenubarPrimitive.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'> <MenubarPrimitive.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>));MenubarItem.displayName = MenubarPrimitive.Item.displayName;
const MenubarCheckboxItem = React.forwardRef< MenubarPrimitive.CheckboxItemRef, MenubarPrimitive.CheckboxItemProps>(({ className, children, checked, ...props }, ref) => ( <MenubarPrimitive.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 )} checked={checked} {...props} > <View className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'> <MenubarPrimitive.ItemIndicator> <Check size={14} strokeWidth={3} className='text-foreground' /> </MenubarPrimitive.ItemIndicator> </View> <>{children}</> </MenubarPrimitive.CheckboxItem>));MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
const MenubarRadioItem = React.forwardRef< MenubarPrimitive.RadioItemRef, MenubarPrimitive.RadioItemProps>(({ className, children, ...props }, ref) => ( <MenubarPrimitive.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'> <MenubarPrimitive.ItemIndicator> <View className='bg-foreground h-2 w-2 rounded-full' /> </MenubarPrimitive.ItemIndicator> </View> <>{children}</> </MenubarPrimitive.RadioItem>));MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
const MenubarLabel = React.forwardRef< MenubarPrimitive.LabelRef, MenubarPrimitive.LabelProps & { inset?: boolean; }>(({ className, inset, ...props }, ref) => ( <MenubarPrimitive.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} />));MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
const MenubarSeparator = React.forwardRef< MenubarPrimitive.SeparatorRef, MenubarPrimitive.SeparatorProps>(({ className, ...props }, ref) => ( <MenubarPrimitive.Separator ref={ref} className={cn('-mx-1 my-1 h-px bg-border', className)} {...props} />));MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
const MenubarShortcut = ({ className, ...props }: TextProps) => { return ( <Text className={cn( 'ml-auto text-xs native:text-sm tracking-widest text-muted-foreground', className )} {...props} /> );};MenubarShortcut.displayName = 'MenubarShortcut';
export { Menubar, MenubarCheckboxItem, MenubarContent, MenubarGroup, MenubarItem, MenubarLabel, MenubarMenu, MenubarPortal, MenubarRadioGroup, MenubarRadioItem, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger,};
Usage
import { useNavigation } from 'expo-router';import * as React from 'react';import { Pressable, StyleSheet, View } from 'react-native';import Animated, { FadeIn } from 'react-native-reanimated';import { useSafeAreaInsets } from 'react-native-safe-area-context';import { Menubar, MenubarCheckboxItem, MenubarContent, MenubarItem, MenubarMenu, MenubarRadioGroup, MenubarRadioItem, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger,} from '~/components/ui/menubar';import { Text } from '~/components/ui/text';
function Example() { const insets = useSafeAreaInsets(); const contentInsets = { top: insets.top, bottom: insets.bottom, left: 12, right: 12, }; const [value, setValue] = React.useState<string | undefined>(); const [isSubOpen, setIsSubOpen] = React.useState(false); const [isSubOpen2, setIsSubOpen2] = React.useState(false); const [isChecked, setIsChecked] = React.useState(false); const [isChecked2, setIsChecked2] = React.useState(false); const [radio, setRadio] = React.useState('michael'); const navigation = useNavigation(); React.useEffect(() => { const sub = navigation.addListener('blur', () => { onValueChange(undefined); });
return sub; }, []);
function closeSubs() { setIsSubOpen(false); setIsSubOpen2(false); }
function onValueChange(val: string | undefined) { if (typeof val === 'string') { setValue(val); return; } closeSubs(); setValue(undefined); }
return ( <View className='flex-1 items-center p-4'> {!!value && ( <Pressable onPress={() => { onValueChange(undefined); }} style={StyleSheet.absoluteFill} /> )} <Menubar value={value} onValueChange={onValueChange}> <MenubarMenu value='file'> <MenubarTrigger onPress={closeSubs}> <Text>File</Text> </MenubarTrigger> <MenubarContent insets={contentInsets}> <MenubarItem> <Text>New Tab</Text> <MenubarShortcut>⌘T</MenubarShortcut> </MenubarItem> <MenubarItem> <Text>New Window</Text> <MenubarShortcut>⌘N</MenubarShortcut> </MenubarItem> <MenubarItem disabled> <Text>New Incognito Window</Text> </MenubarItem> <MenubarSeparator /> <MenubarSub open={isSubOpen} onOpenChange={setIsSubOpen}> <MenubarSubTrigger> <Text>Share</Text> </MenubarSubTrigger> <MenubarSubContent> <Animated.View entering={FadeIn.duration(200)}> <MenubarItem> <Text>Email link</Text> </MenubarItem> <MenubarItem> <Text>Messages</Text> </MenubarItem> <MenubarItem> <Text>Notes</Text> </MenubarItem> </Animated.View> </MenubarSubContent> </MenubarSub> <MenubarSeparator /> <MenubarItem> <Text>Print...</Text> <MenubarShortcut>⌘P</MenubarShortcut> </MenubarItem> </MenubarContent> </MenubarMenu> <MenubarMenu value='edit'> <MenubarTrigger onPress={closeSubs}> <Text>Edit</Text> </MenubarTrigger> <MenubarContent insets={contentInsets} className='native:w-48'> <MenubarItem> <Text>Undo</Text> <MenubarShortcut>⌘Z</MenubarShortcut> </MenubarItem> <MenubarItem> <Text>Redo</Text> <MenubarShortcut>⇧⌘Z</MenubarShortcut> </MenubarItem> <MenubarSeparator /> <MenubarSub open={isSubOpen2} onOpenChange={setIsSubOpen2}> <MenubarSubTrigger> <Text>Find</Text> </MenubarSubTrigger> <MenubarSubContent> <Animated.View entering={FadeIn.duration(200)}> <MenubarItem> <Text>Search the web</Text> </MenubarItem> <MenubarSeparator /> <MenubarItem> <Text>Find...</Text> </MenubarItem> <MenubarItem> <Text>Find Next</Text> </MenubarItem> <MenubarItem> <Text>Find Previous</Text> </MenubarItem> </Animated.View> </MenubarSubContent> </MenubarSub> <MenubarSeparator /> <MenubarItem> <Text>Cut</Text> </MenubarItem> <MenubarItem> <Text>Copy</Text> </MenubarItem> <MenubarItem> <Text>Paste</Text> </MenubarItem> </MenubarContent> </MenubarMenu> <MenubarMenu value='view'> <MenubarTrigger onPress={closeSubs}> <Text>View</Text> </MenubarTrigger> <MenubarContent insets={contentInsets}> <MenubarCheckboxItem checked={isChecked} onCheckedChange={setIsChecked} closeOnPress={false} > <Text>Always Show Bookmarks Bar</Text> </MenubarCheckboxItem> <MenubarCheckboxItem checked={isChecked2} onCheckedChange={setIsChecked2} closeOnPress={false} > <Text>Always Show Full URLs</Text> </MenubarCheckboxItem> <MenubarSeparator /> <MenubarItem inset> <Text>Reload</Text> <MenubarShortcut>⌘R</MenubarShortcut> </MenubarItem> <MenubarItem disabled inset> <Text>Force Reload</Text> <MenubarShortcut>⇧⌘R</MenubarShortcut> </MenubarItem> <MenubarSeparator /> <MenubarItem inset> <Text>Toggle Fullscreen</Text> </MenubarItem> <MenubarSeparator /> <MenubarItem inset> <Text>Hide Sidebar</Text> </MenubarItem> </MenubarContent> </MenubarMenu> <MenubarMenu value='profile'> <MenubarTrigger onPress={closeSubs}> <Text>Profiles</Text> </MenubarTrigger> <MenubarContent insets={contentInsets}> <MenubarRadioGroup value={radio} onValueChange={setRadio}> <MenubarRadioItem closeOnPress={false} value='andy'> <Text>Andy</Text> </MenubarRadioItem> <MenubarRadioItem closeOnPress={false} value='michael'> <Text>Michael</Text> </MenubarRadioItem> <MenubarRadioItem closeOnPress={false} value='creed'> <Text>Creed</Text> </MenubarRadioItem> </MenubarRadioGroup> <MenubarSeparator /> <MenubarItem inset> <Text>Edit...</Text> </MenubarItem> <MenubarSeparator /> <MenubarItem inset> <Text>Add Profile...</Text> </MenubarItem> </MenubarContent> </MenubarMenu> </Menubar> </View> );}
Props
Menubar
Extends View
props
Prop | Type | Note |
---|---|---|
open* | boolean | |
onOpenChange* | (value: boolean) => void | |
asChild | boolean | (optional) |
MenubarMenu
Extends View
props
Prop | Type | Note |
---|---|---|
value* | string | |
asChild | boolean | (optional) |
MenubarTrigger
Extends Pressable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
inset | boolean | (optional) |
MenubarPortal
Prop | Type | Note |
---|---|---|
children* | React.ReactNode | |
forceMount | true | undefined | (optional) |
hostName | string | Web Only (optional) |
container | HTMLElement | null | undefined | Web Only (optional) |
MenubarContent
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) |
MenubarGroup
Extends Text
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
MenubarLabel
Extends Text
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
MenubarItem
Extends Pressable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
textValue | boolean | (optional) |
closeOnPress | boolean | (optional) |
MenubarCheckboxItem
Extends Pressable
props
Prop | Type | Note |
---|---|---|
checked* | boolean | |
onCheckedChange* | (value: boolean) => void | |
textValue* | string | |
asChild | boolean | (optional) |
closeOnPress | boolean | Native Only (optional) |
MenubarRadioGroup
Extends View
props
Prop | Type | Note |
---|---|---|
value* | boolean | |
onValueChange* | (value: boolean) => void | |
asChild | boolean | (optional) |
MenubarRadioItem
Extends Pressable
props
Prop | Type | Note |
---|---|---|
value* | boolean | |
onCheckedChange* | (value: boolean) => void | |
asChild | boolean | (optional) |
closeOnPress | boolean | Native Only (optional) |
MenubarSeparator
Extends View
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
decorative | boolean | (optional) |
MenubarSub
Extends View
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
defaultOpen | boolean | (optional) |
open | boolean | (optional) |
onOpenChange | (value: boolean) => void | (optional) |
MenubarSubTrigger
Extends Pressable
props
Prop | Type | Note |
---|---|---|
textValue | string | (optional) |
asChild | boolean | (optional) |
MenubarSubContent
Extends Pressable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
forceMount | true / | undefined |
MenubarShortcut
Extends Text
props