Menubar Primitive
A menu that stays visible on the screen, often seen in desktop apps, offering easy access to a standard set of commands.
Installation
Install the component via your command line.
npx expo install @rn-primitives/menubar
Install @radix-ui/react-menubar
npx expo install @radix-ui/react-menubar
Copy/paste the following code for web to ~/components/primitives/menubar/menubar.web.tsx
import * as Menubar from '@radix-ui/react-menubar';import { useAugmentedRef, useControllableState, useIsomorphicLayoutEffect,} from '~/components/primitives/hooks';import * as Slot from '~/components/primitives/slot';import { EmptyGestureResponderEvent } from '~/components/primitives/utils';import * as React from 'react';import { GestureResponderEvent, Pressable, Text, View } from 'react-native';import type { CheckboxItemProps, CheckboxItemRef, ContentProps, ContentRef, GroupProps, GroupRef, ItemIndicatorProps, ItemIndicatorRef, ItemProps, ItemRef, LabelProps, LabelRef, MenuProps, MenuRef, OverlayProps, OverlayRef, PortalProps, RadioGroupProps, RadioGroupRef, RadioItemProps, RadioItemRef, RootProps, RootRef, SeparatorProps, SeparatorRef, SubContentProps, SubContentRef, SubProps, SubRef, SubTriggerProps, SubTriggerRef, TriggerProps, TriggerRef,} from './types';
const RootContext = React.createContext<RootProps | null>(null);
const Root = React.forwardRef<RootRef, RootProps>( ({ asChild, value, onValueChange, ...viewProps }, ref) => { const Component = asChild ? Slot.View : View; return ( <RootContext.Provider value={{ value, onValueChange }}> <Menubar.Root value={value} onValueChange={onValueChange}> <Component ref={ref} {...viewProps} /> </Menubar.Root> </RootContext.Provider> ); });
Root.displayName = 'RootWebMenubar';
function useRootContext() { const context = React.useContext(RootContext); if (!context) { throw new Error('Menubar compound components cannot be rendered outside the Menubar component'); } return context;}
const MenuContext = React.createContext<MenuProps | null>(null);
const Menu = React.forwardRef<MenuRef, MenuProps>(({ asChild, value, ...viewProps }, ref) => { const Component = asChild ? Slot.View : View; return ( <MenuContext.Provider value={{ value }}> <Menubar.Menu value={value}> <Component ref={ref} {...viewProps} /> </Menubar.Menu> </MenuContext.Provider> );});
Menu.displayName = 'MenuWebMenubar';
function useMenuContext() { const context = React.useContext(MenuContext); if (!context) { throw new Error('Menubar compound components cannot be rendered outside the Menubar component'); } return context;}
const Trigger = React.forwardRef<TriggerRef, TriggerProps>( ({ asChild, disabled = false, ...props }, ref) => { const augmentedRef = useAugmentedRef({ ref }); const { value: menuValue } = useMenuContext(); const { value } = useRootContext();
useIsomorphicLayoutEffect(() => { if (augmentedRef.current) { const augRef = augmentedRef.current as unknown as HTMLDivElement; augRef.dataset.state = value && menuValue === value ? 'open' : 'closed'; } }, [value && menuValue]);
useIsomorphicLayoutEffect(() => { if (augmentedRef.current) { const augRef = augmentedRef.current as unknown as HTMLDivElement; if (disabled) { augRef.dataset.disabled = 'true'; } else { augRef.dataset.disabled = undefined; } } }, [disabled]);
const Component = asChild ? Slot.Pressable : Pressable; return ( <Menubar.Trigger disabled={disabled ?? undefined} asChild> <Component ref={augmentedRef} disabled={disabled} {...props} /> </Menubar.Trigger> ); });
Trigger.displayName = 'TriggerWebMenubar';
function Portal({ forceMount, container, children }: PortalProps) { return <Menubar.Portal forceMount={forceMount} container={container} children={children} />;}
const Overlay = React.forwardRef<OverlayRef, OverlayProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.Pressable : Pressable; return <Component ref={ref} {...props} />;});
Overlay.displayName = 'OverlayWebMenubar';
const MenubarContentContext = React.createContext<{ close: () => void;} | null>(null);
const Content = React.forwardRef<ContentRef, ContentProps>( ( { asChild = false, forceMount, align, side, sideOffset, alignOffset = 0, avoidCollisions = true, insets, loop, onCloseAutoFocus, onEscapeKeyDown, onPointerDownOutside, onFocusOutside, onInteractOutside, collisionBoundary, sticky, hideWhenDetached, ...props }, ref ) => { const itemRef = React.useRef<HTMLDivElement>(null);
function close() { itemRef.current?.click(); }
const Component = asChild ? Slot.View : View; return ( <MenubarContentContext.Provider value={{ close }}> <Menubar.Content forceMount={forceMount} alignOffset={alignOffset} avoidCollisions={avoidCollisions} collisionPadding={insets} loop={loop} onCloseAutoFocus={onCloseAutoFocus} onEscapeKeyDown={onEscapeKeyDown} onPointerDownOutside={onPointerDownOutside} onFocusOutside={onFocusOutside} onInteractOutside={onInteractOutside} collisionBoundary={collisionBoundary} sticky={sticky} hideWhenDetached={hideWhenDetached} align={align} side={side} sideOffset={sideOffset} > <Component ref={ref} {...props} /> <Menubar.Item ref={itemRef} aria-hidden style={{ position: 'fixed', top: 0, left: 0, zIndex: -999999999 }} aria-disabled tabIndex={-1} hidden /> </Menubar.Content> </MenubarContentContext.Provider> ); });
Content.displayName = 'ContentWebMenubar';
function useMenubarContentContext() { const context = React.useContext(MenubarContentContext); if (!context) { throw new Error( 'MenubarContent compound components cannot be rendered outside the MenubarContent component' ); } return context;}
const Item = React.forwardRef<ItemRef, ItemProps>( ( { asChild, textValue, closeOnPress = true, onPress: onPressProp, onKeyDown: onKeyDownProp, ...props }, ref ) => { const { close } = useMenubarContentContext();
function onKeyDown(ev: React.KeyboardEvent) { onKeyDownProp?.(ev); if (ev.key === 'Enter' || ev.key === ' ') { onPressProp?.(EmptyGestureResponderEvent); if (closeOnPress) { close(); } } }
function onPress(ev: GestureResponderEvent) { onPressProp?.(ev); if (closeOnPress) { close(); } }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Menubar.Item textValue={textValue} disabled={props.disabled ?? undefined} onSelect={closeOnPress ? undefined : onSelected} asChild > <Component ref={ref} // @ts-expect-error web only onKeyDown={onKeyDown} onPress={onPress} {...props} /> </Menubar.Item> ); });
Item.displayName = 'ItemWebMenubar';
const Group = React.forwardRef<GroupRef, GroupProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.View : View; return ( <Menubar.Group asChild> <Component ref={ref} {...props} /> </Menubar.Group> );});
Group.displayName = 'GroupWebMenubar';
const Label = React.forwardRef<LabelRef, LabelProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.Text : Text; return ( <Menubar.Label asChild> <Component ref={ref} {...props} /> </Menubar.Label> );});
Label.displayName = 'LabelWebMenubar';
const CheckboxItem = React.forwardRef<CheckboxItemRef, CheckboxItemProps>( ( { asChild, checked, onCheckedChange, textValue, disabled = false, closeOnPress = true, onPress: onPressProp, onKeyDown: onKeyDownProp, ...props }, ref ) => { function onKeyDown(ev: React.KeyboardEvent) { onKeyDownProp?.(ev); if (ev.key === 'Enter' || ev.key === ' ') { onPressProp?.(EmptyGestureResponderEvent); onCheckedChange?.(!checked); if (closeOnPress) { close(); } } }
function onPress(ev: GestureResponderEvent) { onPressProp?.(ev); onCheckedChange?.(!checked); if (closeOnPress) { close(); } } const Component = asChild ? Slot.Pressable : Pressable; return ( <Menubar.CheckboxItem textValue={textValue} checked={checked} onCheckedChange={onCheckedChange} onSelect={closeOnPress ? undefined : onSelected} disabled={disabled ?? undefined} asChild > <Component ref={ref} // @ts-expect-error web only onKeyDown={onKeyDown} onPress={onPress} role='button' {...props} /> </Menubar.CheckboxItem> ); });
CheckboxItem.displayName = 'CheckboxItemWebMenubar';
const MenubarRadioGroupContext = React.createContext<{ value?: string; onValueChange?: (value: string) => void;} | null>(null);
const RadioGroup = React.forwardRef<RadioGroupRef, RadioGroupProps>( ({ asChild, value, onValueChange, ...props }, ref) => { const Component = asChild ? Slot.View : View; return ( <MenubarRadioGroupContext.Provider value={{ value, onValueChange }}> <Menubar.RadioGroup value={value} onValueChange={onValueChange} asChild> <Component ref={ref} {...props} /> </Menubar.RadioGroup> </MenubarRadioGroupContext.Provider> ); });
RadioGroup.displayName = 'RadioGroupWebMenubar';
function useMenubarRadioGroupContext() { const context = React.useContext(MenubarRadioGroupContext); if (!context) { throw new Error( 'MenubarRadioGroup compound components cannot be rendered outside the MenubarRadioGroup component' ); } return context;}
const RadioItem = React.forwardRef<RadioItemRef, RadioItemProps>( ( { asChild, value, textValue, closeOnPress = true, onPress: onPressProp, onKeyDown: onKeyDownProp, ...props }, ref ) => { const { onValueChange } = useMenubarRadioGroupContext(); const { close } = useMenubarContentContext();
function onKeyDown(ev: React.KeyboardEvent) { onKeyDownProp?.(ev); if (ev.key === 'Enter' || ev.key === ' ') { onValueChange?.(value); onPressProp?.(EmptyGestureResponderEvent); if (closeOnPress) { close(); } } }
function onPress(ev: GestureResponderEvent) { onValueChange?.(value); onPressProp?.(ev); if (closeOnPress) { close(); } } const Component = asChild ? Slot.Pressable : Pressable; return ( <Menubar.RadioItem value={value} textValue={textValue} disabled={props.disabled ?? undefined} onSelect={closeOnPress ? undefined : onSelected} asChild > <Component ref={ref} // @ts-expect-error web only onKeyDown={onKeyDown} onPress={onPress} {...props} /> </Menubar.RadioItem> ); });
RadioItem.displayName = 'RadioItemWebMenubar';
const ItemIndicator = React.forwardRef<ItemIndicatorRef, ItemIndicatorProps>( ({ asChild, forceMount, ...props }, ref) => { const Component = asChild ? Slot.View : View; return ( <Menubar.ItemIndicator forceMount={forceMount} asChild> <Component ref={ref} {...props} /> </Menubar.ItemIndicator> ); });
ItemIndicator.displayName = 'ItemIndicatorWebMenubar';
const Separator = React.forwardRef<SeparatorRef, SeparatorProps>( ({ asChild, decorative, ...props }, ref) => { const Component = asChild ? Slot.View : View; return ( <Menubar.Separator asChild> <Component ref={ref} {...props} /> </Menubar.Separator> ); });
Separator.displayName = 'SeparatorWebMenubar';
const MenubarSubContext = React.createContext<{ open: boolean; onOpenChange: (open: boolean) => void;} | null>(null);
const Sub = React.forwardRef<SubRef, SubProps>( ({ asChild, defaultOpen, open: openProp, onOpenChange: onOpenChangeProp, ...props }, ref) => { const [open = false, onOpenChange] = useControllableState({ prop: openProp, defaultProp: defaultOpen, onChange: onOpenChangeProp, }); const Component = asChild ? Slot.View : View; return ( <MenubarSubContext.Provider value={{ open, onOpenChange }}> <Menubar.Sub open={open} onOpenChange={onOpenChange}> <Component ref={ref} {...props} /> </Menubar.Sub> </MenubarSubContext.Provider> ); });
Sub.displayName = 'SubWebMenubar';
function useSubContext() { const context = React.useContext(MenubarSubContext); if (!context) { throw new Error( 'MenubarSub compound components cannot be rendered outside the MenubarSub component' ); } return context;}
const SubTrigger = React.forwardRef<SubTriggerRef, SubTriggerProps>( ({ asChild, textValue, disabled = false, onPress: onPressProp, ...props }, ref) => { const { onOpenChange } = useSubContext();
function onPress(ev: GestureResponderEvent) { onOpenChange(true); onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Menubar.SubTrigger disabled={disabled ?? undefined} textValue={textValue} asChild> <Component ref={ref} onPress={onPress} {...props} /> </Menubar.SubTrigger> ); });
SubTrigger.displayName = 'SubTriggerWebMenubar';
const SubContent = React.forwardRef<SubContentRef, SubContentProps>( ({ asChild = false, forceMount, ...props }, ref) => { const Component = asChild ? Slot.View : View; return ( <Menubar.Portal> <Menubar.SubContent forceMount={forceMount}> <Component ref={ref} {...props} /> </Menubar.SubContent> </Menubar.Portal> ); });
Content.displayName = 'ContentWebMenubar';
export { CheckboxItem, Content, Group, Item, ItemIndicator, Label, Menu, Overlay, Portal, RadioGroup, RadioItem, Root, Separator, Sub, SubContent, SubTrigger, Trigger, useMenuContext, useRootContext, useSubContext,};
function onSelected(ev: Event) { ev.preventDefault();}
Copy/paste the following code for native to ~/components/primitives/menubar/menubar.tsx
import { useAugmentedRef, useControllableState, useRelativePosition, type LayoutPosition,} from '~/components/primitives/hooks';import { Portal as RNPPortal } from '~/components/primitives/portal';import * as Slot from '~/components/primitives/slot';import * as React from 'react';import { BackHandler, Pressable, Text, View, type GestureResponderEvent, type LayoutChangeEvent, type LayoutRectangle,} from 'react-native';import type { CheckboxItemProps, CheckboxItemRef, ContentProps, ContentRef, GroupProps, GroupRef, ItemIndicatorProps, ItemIndicatorRef, ItemProps, ItemRef, LabelProps, LabelRef, MenuProps, MenuRef, OverlayProps, OverlayRef, PortalProps, RadioGroupProps, RadioGroupRef, RadioItemProps, RadioItemRef, RootProps, RootRef, SeparatorProps, SeparatorRef, SubContentProps, SubContentRef, SubProps, SubRef, SubTriggerProps, SubTriggerRef, TriggerProps, TriggerRef,} from './types';
interface IMenuContext extends RootProps { triggerPosition: LayoutPosition | null; setTriggerPosition: (triggerPosition: LayoutPosition | null) => void; contentLayout: LayoutRectangle | null; setContentLayout: (contentLayout: LayoutRectangle | null) => void; nativeID: string;}
const RootContext = React.createContext<IMenuContext | null>(null);
const Root = React.forwardRef<RootRef, RootProps>( ({ asChild, value, onValueChange, ...viewProps }, ref) => { const nativeID = React.useId(); const [triggerPosition, setTriggerPosition] = React.useState<LayoutPosition | null>(null); const [contentLayout, setContentLayout] = React.useState<LayoutRectangle | null>(null);
const Component = asChild ? Slot.View : View; return ( <RootContext.Provider value={{ value, onValueChange, nativeID, contentLayout, setContentLayout, setTriggerPosition, triggerPosition, }} > <Component ref={ref} {...viewProps} /> </RootContext.Provider> ); });
Root.displayName = 'RootMenubar';
function useRootContext() { const context = React.useContext(RootContext); if (!context) { throw new Error('Menubar compound components cannot be rendered outside the Menubar component'); } return context;}
const MenuContext = React.createContext<MenuProps | null>(null);
const Menu = React.forwardRef<MenuRef, MenuProps>(({ asChild, value, ...viewProps }, ref) => { const Component = asChild ? Slot.View : View; return ( <MenuContext.Provider value={{ value, }} > <Component ref={ref} role='menubar' {...viewProps} /> </MenuContext.Provider> );});
Menu.displayName = 'MenuMenubar';
function useMenuContext() { const context = React.useContext(MenuContext); if (!context) { throw new Error('Menubar compound components cannot be rendered outside the Menubar component'); } return context;}
const Trigger = React.forwardRef<TriggerRef, TriggerProps>( ({ asChild, onPress: onPressProp, disabled = false, ...props }, ref) => { const triggerRef = useAugmentedRef({ ref }); const { value, onValueChange, setTriggerPosition } = useRootContext(); const { value: menuValue } = useMenuContext();
function onPress(ev: GestureResponderEvent) { if (disabled) return; triggerRef.current?.measure((_x, _y, width, height, pageX, pageY) => { setTriggerPosition({ width, pageX, pageY, height }); });
onValueChange(menuValue === value ? undefined : menuValue); onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={triggerRef} aria-disabled={disabled ?? undefined} role='button' onPress={onPress} disabled={disabled ?? undefined} aria-expanded={value === menuValue} {...props} /> ); });
Trigger.displayName = 'TriggerMenubar';
/** * @warning when using a custom `<PortalHost />`, you will have to adjust the Content's sideOffset to account for nav elements like headers. */function Portal({ forceMount, hostName, children }: PortalProps) { const menubar = useRootContext(); const menu = useMenuContext();
if (!menubar.triggerPosition) { return null; }
if (!forceMount) { if (menubar.value !== menu.value) { return null; } }
return ( <RNPPortal hostName={hostName} name={`${menubar.nativeID}_portal`}> <RootContext.Provider value={menubar} key={`RootContext_${menubar.nativeID}_portal_provider`}> <MenuContext.Provider value={menu} key={`MenuContext_${menubar.nativeID}_portal_provider`}> {children} </MenuContext.Provider> </RootContext.Provider> </RNPPortal> );}
const Overlay = React.forwardRef<OverlayRef, OverlayProps>( ({ asChild, forceMount, onPress: OnPressProp, closeOnPress = true, ...props }, ref) => { const { value, onValueChange, setContentLayout, setTriggerPosition } = useRootContext();
function onPress(ev: GestureResponderEvent) { if (closeOnPress) { setTriggerPosition(null); setContentLayout(null); onValueChange(undefined); } OnPressProp?.(ev); }
if (!forceMount) { if (!value) { return null; } }
const Component = asChild ? Slot.Pressable : Pressable; return <Component ref={ref} onPress={onPress} {...props} />; });
Overlay.displayName = 'OverlayMenubar';
/** * @info `position`, `top`, `left`, and `maxWidth` style properties are controlled internally. Opt out of this behavior by setting `disablePositioningStyle` to `true`. */const Content = React.forwardRef<ContentRef, ContentProps>( ( { asChild = false, forceMount, align = 'start', side = 'bottom', sideOffset = 0, alignOffset = 0, avoidCollisions = true, onLayout: onLayoutProp, insets, style, disablePositioningStyle, ...props }, ref ) => { const { value, onValueChange, triggerPosition, contentLayout, setContentLayout, nativeID, setTriggerPosition, } = useRootContext(); const { value: menuValue } = useMenuContext();
React.useEffect(() => { const backHandler = BackHandler.addEventListener('hardwareBackPress', () => { setTriggerPosition(null); setContentLayout(null); onValueChange(undefined); return true; });
return () => { setContentLayout(null); backHandler.remove(); }; }, []);
const positionStyle = useRelativePosition({ align, avoidCollisions, triggerPosition, contentLayout, alignOffset, insets, sideOffset, side, disablePositioningStyle, });
function onLayout(event: LayoutChangeEvent) { setContentLayout(event.nativeEvent.layout); onLayoutProp?.(event); }
if (!forceMount) { if (value !== menuValue) { return null; } }
const Component = asChild ? Slot.View : View; return ( <Component ref={ref} role='menu' nativeID={nativeID} aria-modal={true} style={[positionStyle, style]} onLayout={onLayout} onStartShouldSetResponder={onStartShouldSetResponder} {...props} /> ); });
Content.displayName = 'ContentMenubar';
const Item = React.forwardRef<ItemRef, ItemProps>( ( { asChild, textValue, onPress: onPressProp, disabled = false, closeOnPress = true, ...props }, ref ) => { const { onValueChange, setContentLayout, setTriggerPosition } = useRootContext();
function onPress(ev: GestureResponderEvent) { if (closeOnPress) { setTriggerPosition(null); setContentLayout(null); onValueChange(undefined); } onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={ref} role='menuitem' onPress={onPress} disabled={disabled} aria-valuetext={textValue} aria-disabled={!!disabled} accessibilityState={{ disabled: !!disabled }} {...props} /> ); });
Item.displayName = 'ItemMenubar';
const Group = React.forwardRef<GroupRef, GroupProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.View : View; return <Component ref={ref} role='group' {...props} />;});
Group.displayName = 'GroupMenubar';
const Label = React.forwardRef<LabelRef, LabelProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.Text : Text; return <Component ref={ref} {...props} />;});
Label.displayName = 'LabelMenubar';
type FormItemContext = | { checked: boolean } | { value: string | undefined; onValueChange: (value: string) => void; };
const FormItemContext = React.createContext<FormItemContext | null>(null);
const CheckboxItem = React.forwardRef<CheckboxItemRef, CheckboxItemProps>( ( { asChild, checked, onCheckedChange, textValue, onPress: onPressProp, closeOnPress = true, disabled = false, ...props }, ref ) => { const { onValueChange, setTriggerPosition, setContentLayout, nativeID } = useRootContext();
function onPress(ev: GestureResponderEvent) { onCheckedChange(!checked); if (closeOnPress) { setTriggerPosition(null); setContentLayout(null); onValueChange(undefined); } onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <FormItemContext.Provider value={{ checked }}> <Component ref={ref} role='checkbox' aria-checked={checked} onPress={onPress} disabled={disabled} aria-disabled={!!disabled} aria-valuetext={textValue} accessibilityState={{ disabled: !!disabled }} {...props} /> </FormItemContext.Provider> ); });
CheckboxItem.displayName = 'CheckboxItemMenubar';
function useFormItemContext() { const context = React.useContext(FormItemContext); if (!context) { throw new Error( 'CheckboxItem or RadioItem compound components cannot be rendered outside of a CheckboxItem or RadioItem component' ); } return context;}
const RadioGroup = React.forwardRef<RadioGroupRef, RadioGroupProps>( ({ asChild, value, onValueChange, ...props }, ref) => { const Component = asChild ? Slot.View : View; return ( <FormItemContext.Provider value={{ value, onValueChange }}> <Component ref={ref} role='radiogroup' {...props} /> </FormItemContext.Provider> ); });
RadioGroup.displayName = 'RadioGroupMenubar';
type BothFormItemContext = Exclude<FormItemContext, { checked: boolean }> & { checked: boolean;};
const RadioItemContext = React.createContext({} as { itemValue: string });
const RadioItem = React.forwardRef<RadioItemRef, RadioItemProps>( ( { asChild, value: itemValue, textValue, onPress: onPressProp, disabled = false, closeOnPress = true, ...props }, ref ) => { const { onValueChange: onRootValueChange, setTriggerPosition, setContentLayout, } = useRootContext();
const { value, onValueChange } = useFormItemContext() as BothFormItemContext; function onPress(ev: GestureResponderEvent) { onValueChange(itemValue); if (closeOnPress) { setTriggerPosition(null); setContentLayout(null); onRootValueChange(undefined); } onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <RadioItemContext.Provider value={{ itemValue }}> <Component ref={ref} onPress={onPress} role='radio' aria-checked={value === itemValue} disabled={disabled ?? false} accessibilityState={{ disabled: disabled ?? false, checked: value === itemValue, }} aria-valuetext={textValue} {...props} /> </RadioItemContext.Provider> ); });
RadioItem.displayName = 'RadioItemMenubar';
function useItemIndicatorContext() { return React.useContext(RadioItemContext);}
const ItemIndicator = React.forwardRef<ItemIndicatorRef, ItemIndicatorProps>( ({ asChild, forceMount, ...props }, ref) => { const { itemValue } = useItemIndicatorContext(); const { checked, value } = useFormItemContext() as BothFormItemContext;
if (!forceMount) { if (itemValue == null && !checked) { return null; } if (value !== itemValue) { return null; } } const Component = asChild ? Slot.View : View; return <Component ref={ref} role='presentation' {...props} />; });
ItemIndicator.displayName = 'ItemIndicatorMenubar';
const Separator = React.forwardRef<SeparatorRef, SeparatorProps>( ({ asChild, decorative, ...props }, ref) => { const Component = asChild ? Slot.View : View; return <Component role={decorative ? 'presentation' : 'separator'} ref={ref} {...props} />; });
Separator.displayName = 'SeparatorMenubar';
const SubContext = React.createContext<{ nativeID: string; open: boolean; onOpenChange: (value: boolean) => void;} | null>(null);
const Sub = React.forwardRef<SubRef, SubProps>( ({ asChild, defaultOpen, open: openProp, onOpenChange: onOpenChangeProp, ...props }, ref) => { const nativeID = React.useId(); const [open = false, onOpenChange] = useControllableState({ prop: openProp, defaultProp: defaultOpen, onChange: onOpenChangeProp, });
const Component = asChild ? Slot.View : View; return ( <SubContext.Provider value={{ nativeID, open, onOpenChange, }} > <Component ref={ref} {...props} /> </SubContext.Provider> ); });
Sub.displayName = 'SubMenubar';
function useSubContext() { const context = React.useContext(SubContext); if (!context) { throw new Error('Sub compound components cannot be rendered outside of a Sub component'); } return context;}
const SubTrigger = React.forwardRef<SubTriggerRef, SubTriggerProps>( ({ asChild, textValue, onPress: onPressProp, disabled = false, ...props }, ref) => { const { nativeID, open, onOpenChange } = useSubContext();
function onPress(ev: GestureResponderEvent) { onOpenChange(!open); onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={ref} aria-valuetext={textValue} role='menuitem' aria-expanded={open} accessibilityState={{ expanded: open, disabled: !!disabled }} nativeID={nativeID} onPress={onPress} disabled={disabled} aria-disabled={!!disabled} {...props} /> ); });
SubTrigger.displayName = 'SubTriggerMenubar';
const SubContent = React.forwardRef<SubContentRef, SubContentProps>( ({ asChild = false, forceMount, ...props }, ref) => { const { open, nativeID } = useSubContext();
if (!forceMount) { if (!open) { return null; } }
const Component = asChild ? Slot.View : View; return <Component ref={ref} role='group' aria-labelledby={nativeID} {...props} />; });
SubContent.displayName = 'SubContentMenubar';
export { CheckboxItem, Content, Group, Item, ItemIndicator, Label, Menu, Overlay, Portal, RadioGroup, RadioItem, Root, Separator, Sub, SubContent, SubTrigger, Trigger, useMenuContext, useRootContext, useSubContext,};
function onStartShouldSetResponder() { return true;}
Copy/paste the following code for types to ~/components/primitives/menubar/types.ts
import { ForceMountable, PositionedContentProps, PressableRef, SlottablePressableProps, SlottableTextProps, SlottableViewProps, TextRef, ViewRef,} from '~/components/primitives/types';
type RootProps = SlottableViewProps & { value: string | undefined; onValueChange: (value: string | undefined) => void;};
type MenuProps = SlottableViewProps & { value: string | undefined;};
interface PortalProps extends ForceMountable { children: React.ReactNode; /** * Platform: NATIVE ONLY */ hostName?: string; /** * Platform: WEB ONLY */ container?: HTMLElement | null | undefined;}
type OverlayProps = ForceMountable & SlottablePressableProps & { closeOnPress?: boolean; };
type ItemProps = SlottablePressableProps & { textValue?: string; closeOnPress?: boolean;};
type CheckboxItemProps = SlottablePressableProps & { checked: boolean; onCheckedChange: (checked: boolean) => void; closeOnPress?: boolean; textValue?: string;};
type RadioGroupProps = SlottableViewProps & { value: string | undefined; onValueChange: (value: string) => void;};
type RadioItemProps = SlottablePressableProps & { value: string; textValue?: string; closeOnPress?: boolean;};
type SeparatorProps = SlottableViewProps & { decorative?: boolean;};
type SubProps = SlottableViewProps & { defaultOpen?: boolean; open?: boolean; onOpenChange?: (value: boolean) => void;};
type SubTriggerProps = SlottablePressableProps & { textValue?: string;};
type TriggerProps = SlottablePressableProps;type ContentProps = SlottableViewProps & PositionedContentProps;type SubContentProps = SlottableViewProps & ForceMountable;type ItemIndicatorProps = SlottableViewProps & ForceMountable;type GroupProps = SlottableViewProps;type LabelProps = SlottableTextProps;
type CheckboxItemRef = PressableRef;type ContentRef = ViewRef;type GroupRef = ViewRef;type ItemIndicatorRef = ViewRef;type ItemRef = PressableRef;type LabelRef = TextRef;type MenuRef = ViewRef;type OverlayRef = PressableRef;type RadioGroupRef = ViewRef;type RadioItemRef = PressableRef;type RootRef = ViewRef;type SeparatorRef = ViewRef;type SubContentRef = ViewRef;type SubRef = ViewRef;type SubTriggerRef = PressableRef;type TriggerRef = PressableRef;
export type { CheckboxItemProps, CheckboxItemRef, ContentProps, ContentRef, GroupProps, GroupRef, ItemIndicatorProps, ItemIndicatorRef, ItemProps, ItemRef, LabelProps, LabelRef, MenuProps, MenuRef, OverlayProps, OverlayRef, PortalProps, RadioGroupProps, RadioGroupRef, RadioItemProps, RadioItemRef, RootProps, RootRef, SeparatorProps, SeparatorRef, SubContentProps, SubContentRef, SubProps, SubRef, SubTriggerProps, SubTriggerRef, TriggerProps, TriggerRef,};
Copy/paste the following code for exporting to ~/components/primitives/menubar/index.ts
export * from './menubar';export * from './types';
Copy/paste the following code for native to ~/components/primitives/menubar/index.tsx
import { useAugmentedRef, useControllableState, useRelativePosition, type LayoutPosition,} from '~/components/primitives/hooks';import { Portal as RNPPortal } from '~/components/primitives/portal';import * as Slot from '~/components/primitives/slot';import * as React from 'react';import { BackHandler, Pressable, Text, View, type GestureResponderEvent, type LayoutChangeEvent, type LayoutRectangle,} from 'react-native';import type { CheckboxItemProps, CheckboxItemRef, ContentProps, ContentRef, GroupProps, GroupRef, ItemIndicatorProps, ItemIndicatorRef, ItemProps, ItemRef, LabelProps, LabelRef, MenuProps, MenuRef, OverlayProps, OverlayRef, PortalProps, RadioGroupProps, RadioGroupRef, RadioItemProps, RadioItemRef, RootProps, RootRef, SeparatorProps, SeparatorRef, SubContentProps, SubContentRef, SubProps, SubRef, SubTriggerProps, SubTriggerRef, TriggerProps, TriggerRef,} from './types';
interface IMenuContext extends RootProps { triggerPosition: LayoutPosition | null; setTriggerPosition: (triggerPosition: LayoutPosition | null) => void; contentLayout: LayoutRectangle | null; setContentLayout: (contentLayout: LayoutRectangle | null) => void; nativeID: string;}
const RootContext = React.createContext<IMenuContext | null>(null);
const Root = React.forwardRef<RootRef, RootProps>( ({ asChild, value, onValueChange, ...viewProps }, ref) => { const nativeID = React.useId(); const [triggerPosition, setTriggerPosition] = React.useState<LayoutPosition | null>(null); const [contentLayout, setContentLayout] = React.useState<LayoutRectangle | null>(null);
const Component = asChild ? Slot.View : View; return ( <RootContext.Provider value={{ value, onValueChange, nativeID, contentLayout, setContentLayout, setTriggerPosition, triggerPosition, }} > <Component ref={ref} {...viewProps} /> </RootContext.Provider> ); });
Root.displayName = 'RootMenubar';
function useRootContext() { const context = React.useContext(RootContext); if (!context) { throw new Error('Menubar compound components cannot be rendered outside the Menubar component'); } return context;}
const MenuContext = React.createContext<MenuProps | null>(null);
const Menu = React.forwardRef<MenuRef, MenuProps>(({ asChild, value, ...viewProps }, ref) => { const Component = asChild ? Slot.View : View; return ( <MenuContext.Provider value={{ value, }} > <Component ref={ref} role='menubar' {...viewProps} /> </MenuContext.Provider> );});
Menu.displayName = 'MenuMenubar';
function useMenuContext() { const context = React.useContext(MenuContext); if (!context) { throw new Error('Menubar compound components cannot be rendered outside the Menubar component'); } return context;}
const Trigger = React.forwardRef<TriggerRef, TriggerProps>( ({ asChild, onPress: onPressProp, disabled = false, ...props }, ref) => { const triggerRef = useAugmentedRef({ ref }); const { value, onValueChange, setTriggerPosition } = useRootContext(); const { value: menuValue } = useMenuContext();
function onPress(ev: GestureResponderEvent) { if (disabled) return; triggerRef.current?.measure((_x, _y, width, height, pageX, pageY) => { setTriggerPosition({ width, pageX, pageY, height }); });
onValueChange(menuValue === value ? undefined : menuValue); onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={triggerRef} aria-disabled={disabled ?? undefined} role='button' onPress={onPress} disabled={disabled ?? undefined} aria-expanded={value === menuValue} {...props} /> ); });
Trigger.displayName = 'TriggerMenubar';
/** * @warning when using a custom `<PortalHost />`, you will have to adjust the Content's sideOffset to account for nav elements like headers. */function Portal({ forceMount, hostName, children }: PortalProps) { const menubar = useRootContext(); const menu = useMenuContext();
if (!menubar.triggerPosition) { return null; }
if (!forceMount) { if (menubar.value !== menu.value) { return null; } }
return ( <RNPPortal hostName={hostName} name={`${menubar.nativeID}_portal`}> <RootContext.Provider value={menubar} key={`RootContext_${menubar.nativeID}_portal_provider`}> <MenuContext.Provider value={menu} key={`MenuContext_${menubar.nativeID}_portal_provider`}> {children} </MenuContext.Provider> </RootContext.Provider> </RNPPortal> );}
const Overlay = React.forwardRef<OverlayRef, OverlayProps>( ({ asChild, forceMount, onPress: OnPressProp, closeOnPress = true, ...props }, ref) => { const { value, onValueChange, setContentLayout, setTriggerPosition } = useRootContext();
function onPress(ev: GestureResponderEvent) { if (closeOnPress) { setTriggerPosition(null); setContentLayout(null); onValueChange(undefined); } OnPressProp?.(ev); }
if (!forceMount) { if (!value) { return null; } }
const Component = asChild ? Slot.Pressable : Pressable; return <Component ref={ref} onPress={onPress} {...props} />; });
Overlay.displayName = 'OverlayMenubar';
/** * @info `position`, `top`, `left`, and `maxWidth` style properties are controlled internally. Opt out of this behavior by setting `disablePositioningStyle` to `true`. */const Content = React.forwardRef<ContentRef, ContentProps>( ( { asChild = false, forceMount, align = 'start', side = 'bottom', sideOffset = 0, alignOffset = 0, avoidCollisions = true, onLayout: onLayoutProp, insets, style, disablePositioningStyle, ...props }, ref ) => { const { value, onValueChange, triggerPosition, contentLayout, setContentLayout, nativeID, setTriggerPosition, } = useRootContext(); const { value: menuValue } = useMenuContext();
React.useEffect(() => { const backHandler = BackHandler.addEventListener('hardwareBackPress', () => { setTriggerPosition(null); setContentLayout(null); onValueChange(undefined); return true; });
return () => { setContentLayout(null); backHandler.remove(); }; }, []);
const positionStyle = useRelativePosition({ align, avoidCollisions, triggerPosition, contentLayout, alignOffset, insets, sideOffset, side, disablePositioningStyle, });
function onLayout(event: LayoutChangeEvent) { setContentLayout(event.nativeEvent.layout); onLayoutProp?.(event); }
if (!forceMount) { if (value !== menuValue) { return null; } }
const Component = asChild ? Slot.View : View; return ( <Component ref={ref} role='menu' nativeID={nativeID} aria-modal={true} style={[positionStyle, style]} onLayout={onLayout} onStartShouldSetResponder={onStartShouldSetResponder} {...props} /> ); });
Content.displayName = 'ContentMenubar';
const Item = React.forwardRef<ItemRef, ItemProps>( ( { asChild, textValue, onPress: onPressProp, disabled = false, closeOnPress = true, ...props }, ref ) => { const { onValueChange, setContentLayout, setTriggerPosition } = useRootContext();
function onPress(ev: GestureResponderEvent) { if (closeOnPress) { setTriggerPosition(null); setContentLayout(null); onValueChange(undefined); } onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={ref} role='menuitem' onPress={onPress} disabled={disabled} aria-valuetext={textValue} aria-disabled={!!disabled} accessibilityState={{ disabled: !!disabled }} {...props} /> ); });
Item.displayName = 'ItemMenubar';
const Group = React.forwardRef<GroupRef, GroupProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.View : View; return <Component ref={ref} role='group' {...props} />;});
Group.displayName = 'GroupMenubar';
const Label = React.forwardRef<LabelRef, LabelProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.Text : Text; return <Component ref={ref} {...props} />;});
Label.displayName = 'LabelMenubar';
type FormItemContext = | { checked: boolean } | { value: string | undefined; onValueChange: (value: string) => void; };
const FormItemContext = React.createContext<FormItemContext | null>(null);
const CheckboxItem = React.forwardRef<CheckboxItemRef, CheckboxItemProps>( ( { asChild, checked, onCheckedChange, textValue, onPress: onPressProp, closeOnPress = true, disabled = false, ...props }, ref ) => { const { onValueChange, setTriggerPosition, setContentLayout, nativeID } = useRootContext();
function onPress(ev: GestureResponderEvent) { onCheckedChange(!checked); if (closeOnPress) { setTriggerPosition(null); setContentLayout(null); onValueChange(undefined); } onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <FormItemContext.Provider value={{ checked }}> <Component ref={ref} role='checkbox' aria-checked={checked} onPress={onPress} disabled={disabled} aria-disabled={!!disabled} aria-valuetext={textValue} accessibilityState={{ disabled: !!disabled }} {...props} /> </FormItemContext.Provider> ); });
CheckboxItem.displayName = 'CheckboxItemMenubar';
function useFormItemContext() { const context = React.useContext(FormItemContext); if (!context) { throw new Error( 'CheckboxItem or RadioItem compound components cannot be rendered outside of a CheckboxItem or RadioItem component' ); } return context;}
const RadioGroup = React.forwardRef<RadioGroupRef, RadioGroupProps>( ({ asChild, value, onValueChange, ...props }, ref) => { const Component = asChild ? Slot.View : View; return ( <FormItemContext.Provider value={{ value, onValueChange }}> <Component ref={ref} role='radiogroup' {...props} /> </FormItemContext.Provider> ); });
RadioGroup.displayName = 'RadioGroupMenubar';
type BothFormItemContext = Exclude<FormItemContext, { checked: boolean }> & { checked: boolean;};
const RadioItemContext = React.createContext({} as { itemValue: string });
const RadioItem = React.forwardRef<RadioItemRef, RadioItemProps>( ( { asChild, value: itemValue, textValue, onPress: onPressProp, disabled = false, closeOnPress = true, ...props }, ref ) => { const { onValueChange: onRootValueChange, setTriggerPosition, setContentLayout, } = useRootContext();
const { value, onValueChange } = useFormItemContext() as BothFormItemContext; function onPress(ev: GestureResponderEvent) { onValueChange(itemValue); if (closeOnPress) { setTriggerPosition(null); setContentLayout(null); onRootValueChange(undefined); } onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <RadioItemContext.Provider value={{ itemValue }}> <Component ref={ref} onPress={onPress} role='radio' aria-checked={value === itemValue} disabled={disabled ?? false} accessibilityState={{ disabled: disabled ?? false, checked: value === itemValue, }} aria-valuetext={textValue} {...props} /> </RadioItemContext.Provider> ); });
RadioItem.displayName = 'RadioItemMenubar';
function useItemIndicatorContext() { return React.useContext(RadioItemContext);}
const ItemIndicator = React.forwardRef<ItemIndicatorRef, ItemIndicatorProps>( ({ asChild, forceMount, ...props }, ref) => { const { itemValue } = useItemIndicatorContext(); const { checked, value } = useFormItemContext() as BothFormItemContext;
if (!forceMount) { if (itemValue == null && !checked) { return null; } if (value !== itemValue) { return null; } } const Component = asChild ? Slot.View : View; return <Component ref={ref} role='presentation' {...props} />; });
ItemIndicator.displayName = 'ItemIndicatorMenubar';
const Separator = React.forwardRef<SeparatorRef, SeparatorProps>( ({ asChild, decorative, ...props }, ref) => { const Component = asChild ? Slot.View : View; return <Component role={decorative ? 'presentation' : 'separator'} ref={ref} {...props} />; });
Separator.displayName = 'SeparatorMenubar';
const SubContext = React.createContext<{ nativeID: string; open: boolean; onOpenChange: (value: boolean) => void;} | null>(null);
const Sub = React.forwardRef<SubRef, SubProps>( ({ asChild, defaultOpen, open: openProp, onOpenChange: onOpenChangeProp, ...props }, ref) => { const nativeID = React.useId(); const [open = false, onOpenChange] = useControllableState({ prop: openProp, defaultProp: defaultOpen, onChange: onOpenChangeProp, });
const Component = asChild ? Slot.View : View; return ( <SubContext.Provider value={{ nativeID, open, onOpenChange, }} > <Component ref={ref} {...props} /> </SubContext.Provider> ); });
Sub.displayName = 'SubMenubar';
function useSubContext() { const context = React.useContext(SubContext); if (!context) { throw new Error('Sub compound components cannot be rendered outside of a Sub component'); } return context;}
const SubTrigger = React.forwardRef<SubTriggerRef, SubTriggerProps>( ({ asChild, textValue, onPress: onPressProp, disabled = false, ...props }, ref) => { const { nativeID, open, onOpenChange } = useSubContext();
function onPress(ev: GestureResponderEvent) { onOpenChange(!open); onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={ref} aria-valuetext={textValue} role='menuitem' aria-expanded={open} accessibilityState={{ expanded: open, disabled: !!disabled }} nativeID={nativeID} onPress={onPress} disabled={disabled} aria-disabled={!!disabled} {...props} /> ); });
SubTrigger.displayName = 'SubTriggerMenubar';
const SubContent = React.forwardRef<SubContentRef, SubContentProps>( ({ asChild = false, forceMount, ...props }, ref) => { const { open, nativeID } = useSubContext();
if (!forceMount) { if (!open) { return null; } }
const Component = asChild ? Slot.View : View; return <Component ref={ref} role='group' aria-labelledby={nativeID} {...props} />; });
SubContent.displayName = 'SubContentMenubar';
export { CheckboxItem, Content, Group, Item, ItemIndicator, Label, Menu, Overlay, Portal, RadioGroup, RadioItem, Root, Separator, Sub, SubContent, SubTrigger, Trigger, useMenuContext, useRootContext, useSubContext,};
function onStartShouldSetResponder() { return true;}
Copy/paste the following code for types to ~/components/primitives/menubar/types.ts
import { ForceMountable, PositionedContentProps, PressableRef, SlottablePressableProps, SlottableTextProps, SlottableViewProps, TextRef, ViewRef,} from '~/components/primitives/types';
type RootProps = SlottableViewProps & { value: string | undefined; onValueChange: (value: string | undefined) => void;};
type MenuProps = SlottableViewProps & { value: string | undefined;};
interface PortalProps extends ForceMountable { children: React.ReactNode; /** * Platform: NATIVE ONLY */ hostName?: string; /** * Platform: WEB ONLY */ container?: HTMLElement | null | undefined;}
type OverlayProps = ForceMountable & SlottablePressableProps & { closeOnPress?: boolean; };
type ItemProps = SlottablePressableProps & { textValue?: string; closeOnPress?: boolean;};
type CheckboxItemProps = SlottablePressableProps & { checked: boolean; onCheckedChange: (checked: boolean) => void; closeOnPress?: boolean; textValue?: string;};
type RadioGroupProps = SlottableViewProps & { value: string | undefined; onValueChange: (value: string) => void;};
type RadioItemProps = SlottablePressableProps & { value: string; textValue?: string; closeOnPress?: boolean;};
type SeparatorProps = SlottableViewProps & { decorative?: boolean;};
type SubProps = SlottableViewProps & { defaultOpen?: boolean; open?: boolean; onOpenChange?: (value: boolean) => void;};
type SubTriggerProps = SlottablePressableProps & { textValue?: string;};
type TriggerProps = SlottablePressableProps;type ContentProps = SlottableViewProps & PositionedContentProps;type SubContentProps = SlottableViewProps & ForceMountable;type ItemIndicatorProps = SlottableViewProps & ForceMountable;type GroupProps = SlottableViewProps;type LabelProps = SlottableTextProps;
type CheckboxItemRef = PressableRef;type ContentRef = ViewRef;type GroupRef = ViewRef;type ItemIndicatorRef = ViewRef;type ItemRef = PressableRef;type LabelRef = TextRef;type MenuRef = ViewRef;type OverlayRef = PressableRef;type RadioGroupRef = ViewRef;type RadioItemRef = PressableRef;type RootRef = ViewRef;type SeparatorRef = ViewRef;type SubContentRef = ViewRef;type SubRef = ViewRef;type SubTriggerRef = PressableRef;type TriggerRef = PressableRef;
export type { CheckboxItemProps, CheckboxItemRef, ContentProps, ContentRef, GroupProps, GroupRef, ItemIndicatorProps, ItemIndicatorRef, ItemProps, ItemRef, LabelProps, LabelRef, MenuProps, MenuRef, OverlayProps, OverlayRef, PortalProps, RadioGroupProps, RadioGroupRef, RadioItemProps, RadioItemRef, RootProps, RootRef, SeparatorProps, SeparatorRef, SubContentProps, SubContentRef, SubProps, SubRef, SubTriggerProps, SubTriggerRef, TriggerProps, TriggerRef,};
Usage
import * as React from 'react';import { StyleSheet, View, Text } from 'react-native';import Animated, { FadeIn } from 'react-native-reanimated';import * as MenubarPrimitive from '@rn-primitives/menubar';
function Example() { const [value, setValue] = React.useState<string | undefined>(); const [isSubOpen, setIsSubOpen] = React.useState(false); const [isChecked, setIsChecked] = React.useState(false); const [radio, setRadio] = React.useState('michael');
// TODO: handle closing menus when pressing/clicking outside of this Example component. Ex: when navigating to another screen
function closeSubs() { setIsSubOpen(false); }
function onValueChange(val: string | undefined) { if (typeof val === 'string') { setValue(val); return; } closeSubs(); setValue(undefined); }
return ( <> {!!value && ( <MenubarPrimitive.Overlay style={StyleSheet.absoluteFillObject} /> )} <MenubarPrimitive.Root value={value} onValueChange={onValueChange}> <MenubarPrimitive.Menu value='file'> <MenubarPrimitive.Trigger onPress={closeSubs}> <Text>File</Text> </MenubarPrimitive.Trigger> <MenubarPrimitive.Portal> <MenubarPrimitive.Content> <MenubarPrimitive.Item> <Text>New Tab</Text> </MenubarPrimitive.Item> <MenubarPrimitive.Item> <Text>New Window</Text> </MenubarPrimitive.Item> <MenubarPrimitive.Item disabled> <Text>New Incognito Window</Text> </MenubarPrimitive.Item> <MenubarPrimitive.Separator /> <MenubarPrimitive.Sub open={isSubOpen} onOpenChange={setIsSubOpen}> <MenubarPrimitive.SubTrigger> <Text>Share</Text> </MenubarPrimitive.SubTrigger> <MenubarPrimitive.SubContent> <Animated.View entering={FadeIn.duration(200)}> <MenubarPrimitive.Item> <Text>Email link</Text> </MenubarPrimitive.Item> <MenubarPrimitive.Item> <Text>Messages</Text> </MenubarPrimitive.Item> <MenubarPrimitive.Item> <Text>Notes</Text> </MenubarPrimitive.Item> </Animated.View> </MenubarPrimitive.SubContent> </MenubarPrimitive.Sub> <MenubarPrimitive.Separator /> <MenubarPrimitive.Item> <Text>Print...</Text> </MenubarPrimitive.Item> </MenubarPrimitive.Content> </MenubarPrimitive.Portal> </MenubarPrimitive.Menu> <MenubarPrimitive.Menu value='edit'> <MenubarPrimitive.Trigger onPress={closeSubs}> <Text>Edit</Text> </MenubarPrimitive.Trigger> <MenubarPrimitive.Content className='native:w-48'> <MenubarPrimitive.Item> <Text>Undo</Text> </MenubarPrimitive.Item> <MenubarPrimitive.Item> <Text>Redo</Text> </MenubarPrimitive.Item> <MenubarPrimitive.Separator /> <MenubarPrimitive.Item> <Text>Cut</Text> </MenubarPrimitive.Item> <MenubarPrimitive.Item> <Text>Copy</Text> </MenubarPrimitive.Item> <MenubarPrimitive.Item> <Text>Paste</Text> </MenubarPrimitive.Item> </MenubarPrimitive.Content> </MenubarPrimitive.Menu> <MenubarPrimitive.Menu value='view'> <MenubarPrimitive.Trigger onPress={closeSubs}> <Text>View</Text> </MenubarPrimitive.Trigger> <MenubarPrimitive.Portal> <MenubarPrimitive.Content> <MenubarPrimitive.CheckboxItem checked={isChecked} onCheckedChange={setIsChecked} closeOnPress={false} > <Text>Always Show Bookmarks Bar</Text> </MenubarPrimitive.CheckboxItem> <MenubarPrimitive.Item> <Text>Reload</Text> </MenubarPrimitive.Item> <MenubarPrimitive.Item disabled> <Text>Force Reload</Text> </MenubarPrimitive.Item> <MenubarPrimitive.Separator /> <MenubarPrimitive.Item> <Text>Toggle Fullscreen</Text> </MenubarPrimitive.Item> <MenubarPrimitive.Separator /> <MenubarPrimitive.Item> <Text>Hide Sidebar</Text> </MenubarPrimitive.Item> </MenubarPrimitive.Content> </MenubarPrimitive.Portal> </MenubarPrimitive.Menu> <MenubarPrimitive.Menu value='profile'> <MenubarPrimitive.Trigger onPress={closeSubs}> <Text>Profiles</Text> </MenubarPrimitive.Trigger> <MenubarPrimitive.Portal> <MenubarPrimitive.Content> <MenubarPrimitive.RadioGroup value={radio} onValueChange={setRadio}> <MenubarPrimitive.RadioItem closeOnPress={false} value='andy'> <Text>Andy</Text> </MenubarPrimitive.RadioItem> <MenubarPrimitive.RadioItem closeOnPress={false} value='michael'> <Text>Michael</Text> </MenubarPrimitive.RadioItem> <MenubarPrimitive.RadioItem closeOnPress={false} value='creed'> <Text>Creed</Text> </MenubarPrimitive.RadioItem> </MenubarRadioGroup> <MenubarPrimitive.Separator /> <MenubarPrimitive.Item> <Text>Edit...</Text> </MenubarPrimitive.Item> <MenubarPrimitive.Separator /> <MenubarPrimitive.Item> <Text>Add Profile...</Text> </MenubarPrimitive.Item> </MenubarPrimitive.Content> </MenubarPrimitive.Portal> </MenubarPrimitive.Menu> </MenubarPrimitive.Root> </> );}
Props
Root
Extends View
props
Prop | Type | Note |
---|---|---|
value* | boolean | |
onValueChange* | (value: boolean) => void | |
asChild | boolean | (optional) |
Menu
Extends View
props
Prop | Type | Note |
---|---|---|
value* | string | |
asChild | boolean | (optional) |
Trigger
Extends Pressable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
Portal
Prop | Type | Note |
---|---|---|
children* | React.ReactNode | |
forceMount | true | undefined | (optional) |
hostName | string | Web Only (optional) |
container | HTMLElement | null | undefined | Web Only (optional) |
Overlay
Extends Pressable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
forceMount | true | undefined; | (optional) |
Content
Extends View
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (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) |
Group
Extends Text
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
Label
Extends Text
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
Item
Extends Pressable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
textValue | boolean | (optional) |
closeOnPress | boolean | (optional) |
CheckboxItem
Extends Pressable
props
Prop | Type | Note |
---|---|---|
checked* | boolean | |
onCheckedChange* | (value: boolean) => void | |
textValue* | string | |
asChild | boolean | (optional) |
closeOnPress | boolean | Native Only_(optional)_ |
RadioGroup
Extends View
props
Prop | Type | Note |
---|---|---|
value* | boolean | |
onValueChange* | (value: boolean) => void | |
asChild | boolean | (optional) |
RadioItem
Extends Pressable
props
Prop | Type | Note |
---|---|---|
value* | boolean | |
onCheckedChange* | (value: boolean) => void | |
asChild | boolean | (optional) |
closeOnPress | boolean | Native Only_(optional)_ |
ItemIndicator
Extends View
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
forceMount | true / | undefined |
Separator
Extends View
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
decorative | boolean | (optional) |
Sub
Extends View
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
defaultOpen | boolean | (optional) |
open | boolean | (optional) |
onOpenChange | (value: boolean) => void | (optional) |
SubTrigger
Extends Pressable
props
Prop | Type | Note |
---|---|---|
textValue | string | (optional) |
asChild | boolean | (optional) |
SubContent
Extends Pressable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
forceMount | true / | undefined |
useRootContext
Must be used within a Root
component. It provides the following values from the dropdown menu: open
, and onOpenChange
.
useMenuContext
Must be used within a Menu
component. It provides the following values from the dropdown menu: open
, and onOpenChange
.
useSubContext
Must be used within a Sub
component. It provides the following values from the dropdown menu: open
, and onOpenChange
.