component. -->
Presents a selection of options for the user to choose from, activated by a button.
Install the component via your command line.
npx expo install @rn-primitives/select
Install @radix-ui/react-select
npx expo install @radix-ui/react-select
Copy/paste the following code for web to ~/components/primitives/select/select.web.tsx
~/components/primitives/select/select.web.tsx
import * as Select from '@radix-ui/react-select';import { useAugmentedRef, useControllableState, useIsomorphicLayoutEffect,} from '~/components/primitives/hooks';import * as Slot from '~/components/primitives/slot';import * as React from 'react';import { Pressable, Text, View } from 'react-native';import type { ContentProps, ContentRef, GroupProps, GroupRef, ItemIndicatorProps, ItemIndicatorRef, ItemProps, ItemRef, ItemTextProps, ItemTextRef, LabelProps, LabelRef, OverlayProps, OverlayRef, PortalProps, RootProps, RootRef, ScrollDownButtonProps, ScrollUpButtonProps, SeparatorProps, SeparatorRef, SharedRootContext, TriggerProps, TriggerRef, ValueProps, ValueRef, ViewportProps,} from './types'; const SelectContext = React.createContext< | (SharedRootContext & { open: boolean; onOpenChange: (open: boolean) => void; }) | null>(null); /** * @web Parameter of `onValueChange` has the value of `value` for the `value` and the `label` of the selected Option * @ex When an Option with a label of Green Apple, the parameter passed to `onValueChange` is { value: 'green-apple', label: 'green-apple' } */const Root = React.forwardRef<RootRef, RootProps>( ( { asChild, value: valueProp, defaultValue, onValueChange: onValueChangeProp, onOpenChange: onOpenChangeProp, ...viewProps }, ref ) => { const [value, onValueChange] = useControllableState({ prop: valueProp, defaultProp: defaultValue, onChange: onValueChangeProp, }); const [open, setOpen] = React.useState(false); function onOpenChange(value: boolean) { setOpen(value); onOpenChangeProp?.(value); } function onStrValueChange(val: string) { onValueChange({ value: val, label: val }); } const Component = asChild ? Slot.View : View; return ( <SelectContext.Provider value={{ value, onValueChange, open, onOpenChange, }} > <Select.Root value={value?.value} defaultValue={defaultValue?.value} onValueChange={onStrValueChange} open={open} onOpenChange={onOpenChange} > <Component ref={ref} {...viewProps} /> </Select.Root> </SelectContext.Provider> ); }); Root.displayName = 'RootWebSelect'; function useRootContext() { const context = React.useContext(SelectContext); if (!context) { throw new Error('Select compound components cannot be rendered outside the Select component'); } return context;} const Trigger = React.forwardRef<TriggerRef, TriggerProps>( ({ asChild, role: _role, disabled, ...props }, ref) => { const { open, onOpenChange } = useRootContext(); const augmentedRef = useAugmentedRef({ ref, methods: { open() { onOpenChange(true); }, close() { onOpenChange(false); }, }, }); useIsomorphicLayoutEffect(() => { if (augmentedRef.current) { const augRef = augmentedRef.current as unknown as HTMLButtonElement; augRef.dataset.state = open ? 'open' : 'closed'; augRef.type = 'button'; } }, [open]); const Component = asChild ? Slot.Pressable : Pressable; return ( <Select.Trigger disabled={disabled ?? undefined} asChild> <Component ref={augmentedRef} role='button' disabled={disabled} {...props} /> </Select.Trigger> ); }); Trigger.displayName = 'TriggerWebSelect'; const Value = React.forwardRef<ValueRef, ValueProps>( ({ asChild, placeholder, children, ...props }, ref) => { return ( <Slot.Text ref={ref} {...props}> <Select.Value placeholder={placeholder}>{children}</Select.Value> </Slot.Text> ); }); Value.displayName = 'ValueWebSelect'; function Portal({ container, children }: PortalProps) { return <Select.Portal children={children} container={container} />;} const Overlay = React.forwardRef<OverlayRef, OverlayProps>( ({ asChild, forceMount, children, ...props }, ref) => { const { open } = useRootContext(); const Component = asChild ? Slot.Pressable : Pressable; return ( <> {open && <Component ref={ref} {...props} />} {children as React.ReactNode} </> ); }); Overlay.displayName = 'OverlayWebSelect'; const Content = React.forwardRef<ContentRef, ContentProps>( ( { asChild = false, forceMount: _forceMount, align = 'start', side = 'bottom', position = 'popper', sideOffset = 0, alignOffset = 0, avoidCollisions = true, disablePositioningStyle: _disablePositioningStyle, onCloseAutoFocus, onEscapeKeyDown, onInteractOutside: _onInteractOutside, onPointerDownOutside, ...props }, ref ) => { const Component = asChild ? Slot.View : View; return ( <Select.Content onCloseAutoFocus={onCloseAutoFocus} onEscapeKeyDown={onEscapeKeyDown} onPointerDownOutside={onPointerDownOutside} align={align} side={side} sideOffset={sideOffset} alignOffset={alignOffset} avoidCollisions={avoidCollisions} position={position} > <Component ref={ref} {...props} /> </Select.Content> ); }); Content.displayName = 'ContentWebSelect'; const ItemContext = React.createContext<{ itemValue: string; label: string;} | null>(null); const Item = React.forwardRef<ItemRef, ItemProps>( ({ asChild, closeOnPress = true, label, value, children, ...props }, ref) => { return ( <ItemContext.Provider value={{ itemValue: value, label: label }}> <Slot.Pressable ref={ref} {...props}> <Select.Item textValue={label} value={value} disabled={props.disabled ?? undefined}> <>{children as React.ReactNode}</> </Select.Item> </Slot.Pressable> </ItemContext.Provider> ); }); Item.displayName = 'ItemWebSelect'; function useItemContext() { const context = React.useContext(ItemContext); if (!context) { throw new Error('Item compound components cannot be rendered outside of an Item component'); } return context;} const ItemText = React.forwardRef<ItemTextRef, Omit<ItemTextProps, 'children'>>( ({ asChild, ...props }, ref) => { const { label } = useItemContext(); const Component = asChild ? Slot.Text : Text; return ( <Select.ItemText asChild> <Component ref={ref} {...props}> {label} </Component> </Select.ItemText> ); }); ItemText.displayName = 'ItemTextWebSelect'; const ItemIndicator = React.forwardRef<ItemIndicatorRef, ItemIndicatorProps>( ({ asChild, forceMount: _forceMount, ...props }, ref) => { const Component = asChild ? Slot.View : View; return ( <Select.ItemIndicator asChild> <Component ref={ref} {...props} /> </Select.ItemIndicator> ); }); ItemIndicator.displayName = 'ItemIndicatorWebSelect'; const Group = React.forwardRef<GroupRef, GroupProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.View : View; return ( <Select.Group asChild> <Component ref={ref} {...props} /> </Select.Group> );}); Group.displayName = 'GroupWebSelect'; const Label = React.forwardRef<LabelRef, LabelProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.Text : Text; return ( <Select.Label asChild> <Component ref={ref} {...props} /> </Select.Label> );}); Label.displayName = 'LabelWebSelect'; const Separator = React.forwardRef<SeparatorRef, SeparatorProps>( ({ asChild, decorative, ...props }, ref) => { const Component = asChild ? Slot.View : View; return ( <Select.Separator asChild> <Component ref={ref} {...props} /> </Select.Separator> ); }); Separator.displayName = 'SeparatorWebSelect'; const ScrollUpButton = (props: ScrollUpButtonProps) => { return <Select.ScrollUpButton {...props} />;}; const ScrollDownButton = (props: ScrollDownButtonProps) => { return <Select.ScrollDownButton {...props} />;}; const Viewport = (props: ViewportProps) => { return <Select.Viewport {...props} />;}; export { Content, Group, Item, ItemIndicator, ItemText, Label, Overlay, Portal, Root, ScrollDownButton, ScrollUpButton, Separator, Trigger, useItemContext, useRootContext, Value, Viewport,};
Copy/paste the following code for native to ~/components/primitives/select/select.tsx
~/components/primitives/select/select.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 { ContentProps, ContentRef, GroupProps, GroupRef, ItemIndicatorProps, ItemIndicatorRef, ItemProps, ItemRef, ItemTextProps, ItemTextRef, LabelProps, LabelRef, OverlayProps, OverlayRef, PortalProps, RootProps, RootRef, ScrollDownButtonProps, ScrollUpButtonProps, SeparatorProps, SeparatorRef, SharedRootContext, TriggerProps, TriggerRef, ValueProps, ValueRef, ViewportProps,} from './types'; interface IRootContext extends SharedRootContext { open: boolean; onOpenChange: (open: boolean) => void; triggerPosition: LayoutPosition | null; setTriggerPosition: (triggerPosition: LayoutPosition | null) => void; contentLayout: LayoutRectangle | null; setContentLayout: (contentLayout: LayoutRectangle | null) => void; nativeID: string;} const RootContext = React.createContext<IRootContext | null>(null); const Root = React.forwardRef<RootRef, RootProps>( ( { asChild, value: valueProp, defaultValue, onValueChange: onValueChangeProp, onOpenChange: onOpenChangeProp, disabled, ...viewProps }, ref ) => { const nativeID = React.useId(); const [value, onValueChange] = useControllableState({ prop: valueProp, defaultProp: defaultValue, onChange: onValueChangeProp, }); const [triggerPosition, setTriggerPosition] = React.useState<LayoutPosition | null>(null); const [contentLayout, setContentLayout] = React.useState<LayoutRectangle | null>(null); const [open, setOpen] = React.useState(false); function onOpenChange(value: boolean) { setOpen(value); onOpenChangeProp?.(value); } const Component = asChild ? Slot.View : View; return ( <RootContext.Provider value={{ value, onValueChange, open, onOpenChange, disabled, contentLayout, nativeID, setContentLayout, setTriggerPosition, triggerPosition, }} > <Component ref={ref} {...viewProps} /> </RootContext.Provider> ); }); Root.displayName = 'RootNativeSelect'; function useRootContext() { const context = React.useContext(RootContext); if (!context) { throw new Error('Select compound components cannot be rendered outside the Select component'); } return context;} const Trigger = React.forwardRef<TriggerRef, TriggerProps>( ({ asChild, onPress: onPressProp, disabled = false, ...props }, ref) => { const { open, onOpenChange, disabled: disabledRoot, setTriggerPosition } = useRootContext(); const augmentedRef = useAugmentedRef({ ref, methods: { open: () => { onOpenChange(true); augmentedRef.current?.measure((_x, _y, width, height, pageX, pageY) => { setTriggerPosition({ width, pageX, pageY: pageY, height }); }); }, close: () => { setTriggerPosition(null); onOpenChange(false); }, }, }); function onPress(ev: GestureResponderEvent) { if (disabled) return; augmentedRef.current?.measure((_x, _y, width, height, pageX, pageY) => { setTriggerPosition({ width, pageX, pageY: pageY, height }); }); onOpenChange(!open); onPressProp?.(ev); } const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={augmentedRef} aria-disabled={disabled ?? undefined} role='combobox' onPress={onPress} disabled={disabled ?? disabledRoot} aria-expanded={open} {...props} /> ); }); Trigger.displayName = 'TriggerNativeSelect'; const Value = React.forwardRef<ValueRef, ValueProps>(({ asChild, placeholder, ...props }, ref) => { const { value } = useRootContext(); const Component = asChild ? Slot.Text : Text; return ( <Component ref={ref} {...props}> {value?.label ?? placeholder} </Component> );}); Value.displayName = 'ValueNativeSelect'; /** * @warning when using a custom `<PortalHost />`, you might have to adjust the Content's sideOffset. */function Portal({ forceMount, hostName, children }: PortalProps) { const value = useRootContext(); if (!value.triggerPosition) { return null; } if (!forceMount) { if (!value.open) { return null; } } return ( <RNPPortal hostName={hostName} name={`${value.nativeID}_portal`}> <RootContext.Provider value={value}>{children}</RootContext.Provider> </RNPPortal> );} const Overlay = React.forwardRef<OverlayRef, OverlayProps>( ({ asChild, forceMount, onPress: OnPressProp, closeOnPress = true, ...props }, ref) => { const { open, onOpenChange, setTriggerPosition, setContentLayout } = useRootContext(); function onPress(ev: GestureResponderEvent) { if (closeOnPress) { setTriggerPosition(null); setContentLayout(null); onOpenChange(false); } OnPressProp?.(ev); } if (!forceMount) { if (!open) { return null; } } const Component = asChild ? Slot.Pressable : Pressable; return <Component ref={ref} onPress={onPress} {...props} />; }); Overlay.displayName = 'OverlayNativeSelect'; /** * @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, position: _position, ...props }, ref ) => { const { open, onOpenChange, contentLayout, nativeID, triggerPosition, setContentLayout, setTriggerPosition, } = useRootContext(); React.useEffect(() => { const backHandler = BackHandler.addEventListener('hardwareBackPress', () => { setTriggerPosition(null); setContentLayout(null); onOpenChange(false); 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 (!open) { return null; } } const Component = asChild ? Slot.View : View; return ( <Component ref={ref} role='list' nativeID={nativeID} aria-modal={true} style={[positionStyle, style]} onLayout={onLayout} onStartShouldSetResponder={onStartShouldSetResponder} {...props} /> ); }); Content.displayName = 'ContentNativeSelect'; const ItemContext = React.createContext<{ itemValue: string; label: string;} | null>(null); const Item = React.forwardRef<ItemRef, ItemProps>( ( { asChild, value: itemValue, label, onPress: onPressProp, disabled = false, closeOnPress = true, ...props }, ref ) => { const { onOpenChange, value, onValueChange, setTriggerPosition, setContentLayout } = useRootContext(); function onPress(ev: GestureResponderEvent) { if (closeOnPress) { setTriggerPosition(null); setContentLayout(null); onOpenChange(false); } onValueChange({ value: itemValue, label }); onPressProp?.(ev); } const Component = asChild ? Slot.Pressable : Pressable; return ( <ItemContext.Provider value={{ itemValue, label }}> <Component ref={ref} role='option' onPress={onPress} disabled={disabled} aria-checked={value?.value === itemValue} aria-valuetext={label} aria-disabled={!!disabled} accessibilityState={{ disabled: !!disabled, checked: value?.value === itemValue, }} {...props} /> </ItemContext.Provider> ); }); Item.displayName = 'ItemNativeSelect'; function useItemContext() { const context = React.useContext(ItemContext); if (!context) { throw new Error('Item compound components cannot be rendered outside of an Item component'); } return context;} const ItemText = React.forwardRef<ItemTextRef, ItemTextProps>(({ asChild, ...props }, ref) => { const { label } = useItemContext(); const Component = asChild ? Slot.Text : Text; return ( <Component ref={ref} {...props}> {label} </Component> );}); ItemText.displayName = 'ItemTextNativeSelect'; const ItemIndicator = React.forwardRef<ItemIndicatorRef, ItemIndicatorProps>( ({ asChild, forceMount, ...props }, ref) => { const { itemValue } = useItemContext(); const { value } = useRootContext(); if (!forceMount) { if (value?.value !== itemValue) { return null; } } const Component = asChild ? Slot.View : View; return <Component ref={ref} role='presentation' {...props} />; }); ItemIndicator.displayName = 'ItemIndicatorNativeSelect'; const Group = React.forwardRef<GroupRef, GroupProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.View : View; return <Component ref={ref} role='group' {...props} />;}); Group.displayName = 'GroupNativeSelect'; const Label = React.forwardRef<LabelRef, LabelProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.Text : Text; return <Component ref={ref} {...props} />;}); Label.displayName = 'LabelNativeSelect'; 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 = 'SeparatorNativeSelect'; const ScrollUpButton = ({ children }: ScrollUpButtonProps) => { return <>{children}</>;}; const ScrollDownButton = ({ children }: ScrollDownButtonProps) => { return <>{children}</>;}; const Viewport = ({ children }: ViewportProps) => { return <>{children}</>;}; export { Content, Group, Item, ItemIndicator, ItemText, Label, Overlay, Portal, Root, ScrollDownButton, ScrollUpButton, Separator, Trigger, useItemContext, useRootContext, Value, Viewport,}; function onStartShouldSetResponder() { return true;}
Copy/paste the following code for types to ~/components/primitives/select/types.ts
~/components/primitives/select/types.ts
import type { ForceMountable, PositionedContentProps, PressableRef, SlottablePressableProps, SlottableTextProps, SlottableViewProps, TextRef, ViewRef,} from '~/components/primitives/types'; type Option = | { value: string; label: string; } | undefined; interface SharedRootContext { value: Option; onValueChange: (option: Option) => void; disabled?: boolean;} type RootProps = SlottableViewProps & { value?: Option; defaultValue?: Option; onValueChange?: (option: Option) => void; onOpenChange?: (open: boolean) => void; disabled?: boolean; /** * Platform: WEB ONLY */ dir?: 'ltr' | 'rtl'; /** * Platform: WEB ONLY */ name?: string; /** * Platform: WEB ONLY */ required?: boolean;}; type ValueProps = SlottableTextProps & { placeholder: string;}; 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 ContentProps = SlottableViewProps & PositionedContentProps & { /** * Platform: WEB ONLY */ position?: 'popper' | 'item-aligned' | undefined; }; type ItemProps = SlottablePressableProps & { value: string; label: string; closeOnPress?: boolean;}; type TriggerProps = SlottablePressableProps; type ItemTextProps = Omit<SlottableTextProps, 'children'>;type ItemIndicatorProps = SlottableViewProps & ForceMountable;type GroupProps = SlottableViewProps;type LabelProps = SlottableTextProps;type SeparatorProps = SlottableViewProps & { decorative?: boolean;}; /** * PLATFORM: WEB ONLY */type ScrollUpButtonProps = React.ComponentPropsWithoutRef<'div'>;/** * PLATFORM: WEB ONLY */type ScrollDownButtonProps = React.ComponentPropsWithoutRef<'div'>;/** * PLATFORM: WEB ONLY */type ViewportProps = React.ComponentPropsWithoutRef<'div'>; type ContentRef = ViewRef;type GroupRef = ViewRef;type IndicatorRef = ViewRef;type ItemRef = PressableRef;type ItemIndicatorRef = ViewRef;type ItemTextRef = TextRef;type LabelRef = TextRef;type OverlayRef = PressableRef;type RootRef = ViewRef;type SeparatorRef = ViewRef;type TriggerRef = PressableRef & { open: () => void; close: () => void;};type ValueRef = TextRef; export type { ContentProps, ContentRef, GroupProps, GroupRef, IndicatorRef, ItemIndicatorProps, ItemIndicatorRef, ItemProps, ItemRef, ItemTextProps, ItemTextRef, LabelProps, LabelRef, Option, OverlayProps, OverlayRef, PortalProps, RootProps, RootRef, ScrollDownButtonProps, ScrollUpButtonProps, SeparatorProps, SeparatorRef, SharedRootContext, TriggerProps, TriggerRef, ValueProps, ValueRef, ViewportProps,};
Copy/paste the following code for exporting to ~/components/primitives/select/index.ts
~/components/primitives/select/index.ts
export * from './select';export * from './types';
Copy/paste the following code for native to ~/components/primitives/select/index.tsx
~/components/primitives/select/index.tsx
import * as SelectPrimitive from '@rn-primitives/select';import { View } from 'react-native'; function Example() { return ( <SelectPrimitive.Root defaultValue={{ value: 'apple', label: 'Apple' }}> <SelectPrimitive.Trigger > <SelectPrimitive.Value placeholder='Select a fruit' /> </SelectPrimitive.Trigger> <SelectPrimitive.Portal> <SelectPrimitive.Overlay style={StyleSheet.absoluteFill}> <SelectPrimitive.Content> <SelectPrimitive.ScrollUpButton /> <SelectPrimitive.Viewport> <SelectPrimitive.Group> <SelectPrimitive.Label>Fruits</SelectPrimitive.Label> <SelectPrimitive.Item label='Apple' value='apple'> Apple <SelectPrimitive.ItemIndicator /> </SelectPrimitive.Item> <SelectPrimitive.Item label='Banana' value='banana'> Banana <SelectPrimitive.ItemIndicator /> </SelectPrimitive.Item> <SelectPrimitive.Item label='Blueberry' value='blueberry'> Blueberry <SelectPrimitive.ItemIndicator /> </SelectPrimitive.Item> <SelectPrimitive.Item label='Grapes' value='grapes'> Grapes <SelectPrimitive.ItemIndicator /> </SelectPrimitive.Item> <SelectPrimitive.Item label='Pineapple' value='pineapple'> Pineapple <SelectPrimitive.ItemIndicator /> </SelectPrimitive.Item> </SelectPrimitive.Group> </SelectPrimitive.Viewport> <SelectPrimitive.ScrollDownButton /> </SelectPrimitive.Content> </SelectPrimitive.Overlay> </SelectPrimitive.Portal> </SelectPrimitive.Root> );}
Extends View props
View
Extends Pressable props
Pressable
SelectTriggerRef
Extends Text props except children
Text
children
Extends Text props
Web Only: Extends radix’s select ScrollUpButton props
ScrollUpButton
Only renders its children on native
Web Only: Extends radix’s select ScrollDownButton props
ScrollDownButton
Web Only: Extends radix’s select Viewport props
Viewport