Select Primitive
Presents a selection of options for the user to choose from, activated by a button.
Installation
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
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
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
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
export * from './select';export * from './types';
Copy/paste the following code for native to ~/components/primitives/select/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 { 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
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,};
Usage
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> );}
Props
Root
Extends View
props
Prop | Type | Note |
---|---|---|
defaultValue | option: { value: string, label: string } | undefined | (optional) |
value | option: { value: string, label: string } | undefined | (optional) |
onValueChange | (option: { value: string, label: string } | undefined ) => void | (optional) |
onOpenChange | (value: boolean) => void | (optional) |
disabled | boolean | (optional) |
asChild | boolean | (optional) |
dir | ’ltr’ | ‘rtl’ | Web only (optional) |
name | string | Web only (optional) |
required | boolean | Web only (optional) |
Trigger
Extends Pressable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
TYPE: SelectTriggerRef
Methods | args | Note |
---|---|---|
open | opens the select | |
close | closes the select |
Value
Extends Pressable
props
Prop | Type | Note |
---|---|---|
placeholder | string | |
asChild | boolean | (optional) |
Overlay
Extends Pressable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
forceMount | true | undefined; | (optional) |
closeOnPress | 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) |
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) |
position | ’popper’ | ‘item-aligned’ | undefined | Web Only (optional) |
Item
Extends Pressable
props
Prop | Type | Note |
---|---|---|
value | string | |
label | string | |
closeOnPress | boolean | (optional) |
asChild | boolean | (optional) |
ItemText
Extends Text
props except children
ItemIndicator
Extends View
props
Prop | Type | Note |
---|---|---|
forceMount | boolean | (optional) |
Group
Extends View
props
Label
Extends Text
props
Separator
Extends View
props
Prop | Type | Note |
---|---|---|
value | string | |
label | string | |
closeOnPress | boolean | (optional) |
asChild | boolean | (optional) |
ScrollUpButton
Web Only: Extends radix’s select ScrollUpButton
props
Only renders its children on native
ScrollDownButton
Web Only: Extends radix’s select ScrollDownButton
props
Only renders its children on native
Viewport
Web Only: Extends radix’s select Viewport
props
Only renders its children on native