Select

A select component implemented as a native bottom-sheet picker, with single and multiple selection support.

Select is the React Native implementation shipped by the native-select registry item. The component adapts the registry's web-facing composition to native primitives and touch-based interaction.

The native Select adapts the familiar shadcn select composition to React Native. Instead of a browser popover, the options are presented inside the native BottomSheet component.

It supports both single and multiple selection. The selected value is rendered by SelectValue, while SelectItem handles selection and closes the sheet automatically in single-select mode.

See the shadcn/ui Select documentation for the web component API, concepts, and additional examples.

Installation

CLI

npx shadcn add https://ui.tiesen.id.vn/r/native-select.json

Manual

Copy and paste the following code into your project.

import { CheckIcon, ChevronDownIcon } from 'lucide-uniwind'import * as React from 'react'import { ScrollView, View } from 'react-native'import { cn } from '@/lib/utils'import {  BottomSheetContent,  BottomSheet,  BottomSheetTrigger,  BottomSheetHeader,  BottomSheetTitle,} from '@/components/native/bottom-sheet'import { Button } from '@/components/native/button'import { Typography } from '@/components/native/typography'interface SelectContextValue<TValue, TMultiple extends boolean = false> {  value: TMultiple extends true ? TValue[] : TValue | null  onValueChange: (    value: TMultiple extends true ? TValue[] : TValue | null  ) => void  items: readonly { label: string; value: TValue }[]  isMultiple: TMultiple  isOpen: boolean  setIsOpen: React.Dispatch<React.SetStateAction<boolean>>}const SelectContext = React.createContext<SelectContextValue<unknown> | null>(  null)const useSelectContext = <TValue, TMultiple extends boolean = false>() => {  const context = React.useContext(    SelectContext as React.Context<SelectContextValue<TValue, TMultiple> | null>  )  if (!context)    throw new Error('Select components must be wrapped in <Select />')  return context}function Select<TValue, TMultiple extends boolean = false>(  props: Omit<    Partial<SelectContextValue<TValue, TMultiple>>,    'isMultiple' | 'isOpen' | 'setIsOpen'  > & {    multiple?: TMultiple    children: React.ReactNode  }) {  const { items, multiple: isMultiple = false } = props  const [isOpen, setIsOpen] = React.useState(false)  const [localValue, setLocalValue] = React.useState<    TMultiple extends true ? TValue[] : TValue | null  >(props.value ?? ((isMultiple ? [] : null) as never))  const isControlled =    props.value !== undefined && props.onValueChange !== undefined  const value = isControlled ? props.value : localValue  const onValueChange = isControlled ? props.onValueChange : setLocalValue  const memoizedContextValue = React.useMemo(    () => ({ value, onValueChange, items, isMultiple, isOpen, setIsOpen }),    [value, onValueChange, items, isMultiple, isOpen, setIsOpen]  ) as never  return (    <SelectContext value={memoizedContextValue}>      <BottomSheet open={isOpen} onOpenChange={setIsOpen}>        {props.children}      </BottomSheet>    </SelectContext>  )}function SelectTrigger({  className,  children,  ...props}: React.ComponentProps<typeof BottomSheetTrigger>) {  return (    <BottomSheetTrigger      data-slot='select-trigger'      variant='outline'      className={cn('w-full justify-between', className)}      {...props}    >      {children as React.ReactNode}      <ChevronDownIcon className='size-4 shrink-0 text-muted-foreground' />    </BottomSheetTrigger>  )}function SelectValue<TValue, TMultiple extends boolean = false>({  placeholder,  className,  ...props}: React.ComponentProps<typeof Typography> & { placeholder?: string }) {  const { value, items } = useSelectContext<TValue, TMultiple>()  const hasValue =    value === null ||    value === undefined ||    (Array.isArray(value) && value.length === 0)  const displayValue = React.useMemo(() => {    if (hasValue) return placeholder ?? 'Select an option'    if (Array.isArray(value))      return value        .map((val) => items?.find((item) => item.value === val)?.label ?? val)        .join(', ')    return items?.find((item) => item.value === value)?.label ?? value  }, [hasValue, value, items, placeholder])  return (    <Typography      data-slot='select-value'      className={cn('flex-1', hasValue && 'text-muted-foreground', className)}      numberOfLines={1}      {...props}    >      {displayValue.toString()}    </Typography>  )}function SelectContent({  title = 'Select an option',  children,  ...props}: React.ComponentProps<typeof BottomSheetContent> & {  title?: string}) {  const { isMultiple, onValueChange, setIsOpen } = useSelectContext()  return (    <BottomSheetContent data-slot='select-content' {...props}>      <BottomSheetHeader className='flex-row items-center'>        <BottomSheetTitle className='flex-1'>{title}</BottomSheetTitle>        {isMultiple && (          <Button variant='ghost' size='xs' onPress={() => onValueChange([])}>            Clear          </Button>        )}        <Button variant='ghost' size='xs' onPress={() => setIsOpen(false)}>          Done        </Button>      </BottomSheetHeader>      <ScrollView contentContainerClassName='grow px-4 gap-y-1 py-2'>        {children as React.ReactNode}      </ScrollView>    </BottomSheetContent>  )}function SelectItem<TValue>({  value,  onPress,  className,  disabled = false,  children,  ...props}: React.ComponentProps<typeof Button> & { value: TValue }) {  const {    value: selected,    onValueChange,    isMultiple,    setIsOpen,  } = useSelectContext<TValue>()  const handlePress = React.useCallback(    (event: Parameters<NonNullable<typeof onPress>>[0]) => {      if (disabled) return      if (isMultiple && Array.isArray(selected)) {        const newValue = selected?.includes(value)          ? selected?.filter((v) => v !== value)          : [...(selected ?? []), value]        onValueChange(newValue as never)      } else {        onValueChange(value)        setIsOpen(false)      }      onPress?.(event)    },    [disabled, isMultiple, onValueChange, selected, setIsOpen, value, onPress]  )  const isSelected = React.useMemo(() => {    if (isMultiple) return (selected as TValue[])?.includes(value)    return selected === value  }, [isMultiple, selected, value])  return (    <Button      data-slot='select-item'      accessibilityRole='button'      accessibilityState={{ selected: isSelected, disabled }}      aria-disabled={disabled}      disabled={disabled}      variant='ghost'      onPress={handlePress}      className={cn('justify-between', isSelected && 'bg-accent', className)}      {...props}    >      <Typography className={isSelected ? 'text-accent-foreground' : ''}>        {children as React.ReactNode}      </Typography>      {isSelected && (        <CheckIcon className='size-4 shrink-0 text-accent-foreground' />      )}    </Button>  )}function SelectGroup({  className,  ...props}: React.ComponentProps<typeof View>) {  return (    <View      data-slot='select-group'      className={cn('gap-y-1 py-1', className)}      {...props}    />  )}function SelectLabel({  className,  ...props}: React.ComponentProps<typeof Typography>) {  return (    <Typography      data-slot='select-label'      className={cn('py-1 text-xs font-semibold text-foreground', className)}      {...props}    />  )}function SelectSeparator({  className,  ...props}: React.ComponentProps<typeof View>) {  return (    <View      data-slot='select-separator'      className={cn('-mx-1 my-1 h-px bg-border', className)}      {...props}    />  )}export {  Select,  SelectTrigger,  SelectValue,  SelectContent,  SelectItem,  SelectGroup,  SelectLabel,  SelectSeparator,}

Features

  • Native bottom-sheet presentation.
  • Single and multiple selection modes.
  • Controlled and uncontrolled values.
  • Typed option values through generics.
  • Optional groups, labels, and separators.
  • Automatic selected-label rendering.
  • Clear and Done controls in multiple-selection mode.

Registry dependencies: This component composes native-bottom-sheet, native-button, and native-typography.

Usage

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/native/select'

const items = [
  { label: 'Light', value: 'light' },
  { label: 'Dark', value: 'dark' },
  { label: 'System', value: 'system' },
]

export function SelectExample() {
  const [value, setValue] = React.useState('system')

  return (
    <Select
      items={items}
      value={value}
      onValueChange={setValue}
    >
      <SelectTrigger>
        <SelectValue placeholder='Select a theme' />
      </SelectTrigger>

      <SelectContent title='Choose a theme'>
        {items.map((item) => (
          <SelectItem key={item.value} value={item.value}>
            {item.label}
          </SelectItem>
        ))}
      </SelectContent>
    </Select>
  )
}

Single selection

The default mode accepts one value. Selecting an item updates the value and closes the bottom sheet.

<Select
  value={value}
  onValueChange={setValue}
>
  {/* ... */}
</Select>

Multiple selection

Set multiple to allow several values. The sheet stays open while items are toggled; the built-in Clear and Done actions are displayed.

<Select
  multiple
  value={selected}
  onValueChange={setSelected}
>
  {/* ... */}
</Select>

Groups and separators

Use SelectGroup, SelectLabel, and SelectSeparator to organize long option lists.

<SelectContent title='Choose a fruit'>
  <SelectGroup>
    <SelectLabel>Fruit</SelectLabel>
    <SelectItem value='apple'>Apple</SelectItem>
    <SelectItem value='orange'>Orange</SelectItem>
  </SelectGroup>

  <SelectSeparator />

  <SelectGroup>
    <SelectLabel>Vegetables</SelectLabel>
    <SelectItem value='carrot'>Carrot</SelectItem>
  </SelectGroup>
</SelectContent>

Custom option labels

The items array is used by SelectValue to turn selected values back into labels. If a value is not found in items, the raw value is displayed instead.

API Reference

Select

PropTypeDefault
items{ label: string; value: TValue }[]—
valueTValue | TValue[] | nullnull or []
onValueChange(value: TValue | TValue[]) => void—
multiplebooleanfalse
childrenReact.ReactNode—

Select is generic over the option value type. When multiple is true, value is an array.

SelectTrigger

The trigger is built on BottomSheetTrigger.

PropTypeDefault
classNamestring—
childrenReact.ReactNode—
...propsReact.ComponentProps<typeof BottomSheetTrigger>—

SelectValue

PropTypeDefault
placeholderstring"Select an option"
classNamestring—
...propsReact.ComponentProps<typeof Typography>—

SelectContent

PropTypeDefault
titlestring"Select an option"
classNamestring—
...propsReact.ComponentProps<typeof BottomSheetContent>—

SelectItem

PropTypeDefault
valueTValuerequired
disabledbooleanfalse
onPressButton press handler—
childrenReact.ReactNode—
...propsReact.ComponentProps<typeof Button>—

SelectItem must be rendered inside SelectContent.

SelectGroup

Groups related options.

PropTypeDefault
classNamestring—
childrenReact.ReactNode—
...propsReact.ComponentProps<typeof View>—

SelectLabel

Label for a select group.

PropTypeDefault
classNamestring—
childrenReact.ReactNode—
...propsReact.ComponentProps<typeof Typography>—

SelectSeparator

Visual separator between option groups.

PropTypeDefault
classNamestring—
...propsReact.ComponentProps<typeof View>—

On this page