Select

Show a menu of items that users can select from

Select gives users an accessible, keyboard-navigable list of choices and can adapt its content into a Sheet on smaller screens.

Features

  • Customizable, themeable default styling

  • Transitions, themes, and size props

  • Full-featured, accessible, and keyboard navigable

  • Shared Sheet handoff with Dialog and Popover

Select provides a dropdown menu for choosing from a list of options. It’s fully accessible with keyboard navigation, supports typeahead search, and automatically stacks above other content.

In v3, Select keeps adapted sheet content mounted through the sheet exit animation, and web trigger and viewport parts expose data-state="open" | "closed" for styling.

Select also supports ordered multiple selection across custom web lists, browser-native controls, adapted Sheets, and plain React Native content. The same item registry and value controller power every path.

Installation

Select is already installed in tamagui, or you can install it independently:

yarn add @tamagui/select

For native apps, we recommend setting up native portals to preserve React context inside Select content.

Anatomy

// copy-paste: these skins import from files you own. copy each registry item
// below into your app, then adjust the relative import paths to fit.
// components/tamagui/Select.tsx (registry item "select")
// npm dependencies: @tamagui/core @tamagui/select @tamagui/text
import { Select } from "../components/tamagui/Select"; // or '@tamagui/select'
export default () => <Select defaultValue="">
<Select.Trigger>
<Select.Value placeholder="Search..." />
</Select.Trigger>
{/* Optional: Control focus behavior */}
<Select.FocusScope loop trapped focusOnIdle={true}>
<Select.Content>
<Select.ScrollUpButton />
<Select.Viewport>
<Select.Group>
<Select.Label />
<Select.Item>
<Select.ItemText />
</Select.Item>
</Select.Group>
</Select.Viewport>
<Select.ScrollDownButton />
</Select.Content>
</Select.FocusScope>
</Select>;

Multiple selection

Pass multiple to use a controlled or uncontrolled string[]. Selecting a new item appends it. Selecting it again removes it without changing the order of the remaining values. Multiple selection keeps custom lists and adapted Sheets open.

const [fruit, setFruit] = useState<Array<'apple' | 'pear'>>([])
<Select multiple name="fruit" value={fruit} onValueChange={setFruit}>
<Select.Trigger>
<Select.Value placeholder="Choose fruit" />
</Select.Trigger>
<Select.Content>
<Select.Viewport>
<Select.Item value="apple">
<Select.ItemText>Apple</Select.ItemText>
<Select.ItemIndicator />
</Select.Item>
<Select.Item value="pear">
<Select.ItemText>Pear</Select.ItemText>
<Select.ItemIndicator />
</Select.Item>
</Select.Viewport>
</Select.Content>
</Select>

Select.Value renders registered Select.ItemText labels in selection order, separated by , . Use renderValue for chips, localized separators, summaries, or labels that must render before lazy items mount.

<Select multiple value={fruit} renderValue={(values) => `${values.length} selected`} />

Behavior by render path

  • A custom floating web Select is an ARIA multi-select listbox. Arrow keys and typeahead move focus. Enter and Space toggle the focused option.
  • native="web" renders a real <select multiple>. Browser selection, keyboard behavior, and form submission are authoritative.
  • An adapted Sheet uses the same listbox controller on web and stays open after each toggle. The Sheet closes through its overlay, drag-to-bottom behavior, platform back action, or controlled state. Select does not add a Done row.
  • Plain React Native content renders inline when no Adapt target is active. Multiple items expose checkbox-style accessibility state and remain visible after a toggle.

On custom web paths, name renders one root-level hidden input per selected value, including when the collection is portaled into a Sheet. form can point those inputs at an external form. The browser-native path puts name and form on its <select> directly and does not render hidden inputs. React Native does not render HTML form controls.

API reference

Select

Contains every component for the select:

Props

  • id

    string

    Optional for usage with Label.

  • size

    'xs' | 'sm' | 'md' | 'lg' | 'xl'

    Set the size of itself and pass to all inner elements.

  • children

    React.ReactNode

    Select children API components.

  • value

    string | string[]

    Controlled value. With multiple, this is an ordered string array.

  • defaultValue

    string | string[]

    Default value. With multiple, this defaults to an empty array.

  • multiple

    boolean

    Default: 

    false

    Enables ordered multiple selection and makes value callbacks array-valued.

  • onValueChange

    (value, details) => void

    Cancelable value request. Details identify item-press, keyboard, or native-change.

  • open

    boolean

    Controlled open value.

  • defaultOpen

    boolean

    Default open value.

  • onOpenChange

    (open: boolean, details) => void

    Cancelable open request with the interaction reason and source event.

  • dir

    Direction

    Direction of text display.

  • name

    string

    Web form field name. Multiple values submit as repeated entries.

  • form

    string

    Associates the web form control with an external form id.

  • native

    NativeValue

    If passed, will render a native component instead of the custom one. Currently only `web` is supported.

  • renderValue

    (value: string | string[]) => ReactNode

    Render function for the selected value. Multiple mode receives the ordered string array. Useful for SSR, lazy mounting, chips, and custom summaries.

  • lazyMount

    boolean

    Default: 

    false

    When true, defers mounting Select items until opened using React's startTransition. Significantly improves initial render performance for pages with many Selects. Should be combined with `renderValue` for best results.

  • zIndex

    number

    z-index for the select portal. Use when select dropdowns need to appear above other portaled content like dialogs or fixed headers. Defaults to automatic stacking (~100000).

  • Select.Trigger

    Extends ListItem to give sizing, icons, and more. On web it includes data-state="open" | "closed".

    Select.Value

    Extends Paragraph, adding:

    Props

  • placeholder

    string

    Optional placeholder to show when no value selected.

  • Select.Content

    Main container for Select content, used to contain the up/down arrows.

    Props

  • onEscapeKeyDown

    (event: KeyboardEvent) => void

    Called when Escape is pressed while the content is active.

  • onInteractOutside

    (event: Event) => void

    Called when pointer or focus interaction happens outside the content.

  • onPointerDownOutside

    (event: Event) => void

    Called on outside pointer down. Select composes this with its own preventDefault handling.

  • onFocusOutside

    (event: Event) => void

    Called on outside focus. Select composes this with its own preventDefault handling.

  • Select.ScrollUpButton

    Inside Content first, displays when you can scroll up, stuck to the top.

    Extends YStack.

    Select.ScrollDownButton

    Inside Content last, displays when you can scroll down, stuck to the bottom.

    Extends YStack.

    Select.Viewport

    Extends YStack. Contains scrollable content items as children. On web it includes data-state="open" | "closed".

    Props

  • disableScroll

    boolean

    Removes ability to scroll and all style and functionality related to scrolling.

  • unstyled

    boolean

    Removes all default styles.

  • Make sure to not pass height prop as that is managed internally because of UX reasons and having a fixed height will break that behavior.

    Select.Group

    Extends YStack. Use only when grouping together items, alongside a Label as the first child.

    Select.Label

    Extends SizableText. Used to label Groups. Includes size-based padding and minHeight for consistent appearance with other Select items.

    Select.Separator

    Extends Separator. Use inside Select.Group or Select.Viewport to visually divide option groups.

    Select.Item

    Extends ListItem. Used to add selectable string values to the list. Item registration order controls keyboard navigation, typeahead, and selection anchoring.

    Props

  • value

    string

    Provide a value that will be passed on selection.

  • Select.ItemText

    Extends Paragraph. Used inside Item to provide unselectable text that will show above once selected in the parent Select.

    Select.Indicator

    An animated indicator that highlights the currently focused item. Place it inside Select.Viewport to enable a smooth sliding highlight animation as users navigate through options.

    <Select.Viewport>
    <Select.Indicator transition="quick" />
    <Select.Group>{/* items */}</Select.Group>
    </Select.Viewport>

    Use the transition prop to control the animation speed. You can use any animation name from your config like quick, quicker, or quickest.

    By default, Select uses the item’s hover: and press: clauses for hover feedback. Add Select.Indicator for a smoother animated effect. If using the indicator, you may want to set items to have transparent hover styles to avoid visual conflict.

    Select.FocusScope

    Provides access to the underlying FocusScope component used by Select for focus management. Can be used to control focus behavior from a parent component.

    Props

  • enabled

    boolean

    Default: 

    true

    Whether focus management is enabled.

  • loop

    boolean

    Default: 

    false

    When true, tabbing from last item will focus first tabbable and shift+tab from first item will focus last tabbable.

  • trapped

    boolean

    Default: 

    false

    When true, focus cannot escape the focus scope via keyboard, pointer, or programmatic focus.

  • noFocus

    boolean

    Default: 

    false

    Zero focus mode. While active, focus is allowed neither inside nor outside the scope. Web only.

  • focusOnIdle

    boolean | number

    Default: 

    true

    When true, waits for idle before focusing. When a number, waits that many ms. This prevents reflows during animations.

  • onMountAutoFocus

    (event: Event) => void

    Event handler called when auto-focusing on mount. Can be prevented.

  • onUnmountAutoFocus

    (event: Event) => void

    Event handler called when auto-focusing on unmount. Can be prevented.

  • Performance

    For pages with many Select components, you can significantly improve initial render performance by using the lazyMount prop combined with renderValue:

    const labels = {
    apple: 'Apple',
    orange: 'Orange',
    banana: 'Banana',
    }
    <Select lazyMount renderValue={(value) => labels[value]} defaultValue="apple" >
    {/* Items are not mounted until the Select is opened */}
    <Select.Trigger>
    <Select.Value />
    </Select.Trigger>
    <Select.Content>
    <Select.Viewport>
    {/* These items mount in a startTransition when first opened */}
    <Select.Item value="apple">
    <Select.ItemText>Apple</Select.ItemText>
    </Select.Item>
    {/* ... more items */}
    </Select.Viewport>
    </Select.Content>
    </Select>

    How it works:

    • lazyMount defers mounting all Select items until the dropdown is first opened
    • The mount happens inside React’s startTransition, keeping the trigger responsive
    • renderValue provides the display text synchronously, avoiding the need to mount items just to show the selected value
    • Once mounted, items stay mounted for fast subsequent opens

    This is especially useful when rendering many Selects on a single page, as each Select with 20+ items would otherwise mount all those items on initial page load.

    Adapted Sheet

    When used alongside <Adapt />, Select will render as a sheet when that breakpoint is active. See Adapt for how the handoff works.

    This is the only way to render a Select on Native for now, as mobile apps tend to show Select very differently from web and Tamagui wants to present the right abstractions for each platform.

    See Sheet for more props.

    Use Adapt.Contents inside Sheet.Container to insert the contents given to Select.Content.

    // copy-paste: these skins import from files you own. copy each registry item
    // below into your app, then adjust the relative import paths to fit.
    // components/tamagui/Select.tsx (registry item "select")
    // components/tamagui/Sheet.tsx (registry item "sheet")
    // npm dependencies: @tamagui/core @tamagui/select @tamagui/sheet @tamagui/text
    import { Adapt } from 'tamagui';
    import { Select } from "../components/tamagui/Select";
    import { Sheet } from "../components/tamagui/Sheet";
    export default () => <Select defaultValue="">
    <Select.Trigger>
    <Select.Value placeholder="Search..." />
    </Select.Trigger>
    <Adapt when="max-md" platform="touch">
    <Sheet modal dismissOnSnapToBottom>
    <Sheet.Container>
    <Sheet.Background />
    <Adapt.Contents />
    </Sheet.Container>
    <Sheet.Overlay />
    </Sheet>
    </Adapt>
    <Select.Content>
    <Select.ScrollUpButton />
    <Select.Viewport>
    <Select.Group>
    <Select.Label />
    <Select.Item>
    <Select.ItemText />
    </Select.Item>
    <Select.Separator />
    </Select.Group>
    </Select.Viewport>
    <Select.ScrollDownButton />
    </Select.Content>
    </Select>;

    Source

    v2-look Select: styled trigger/value/icon, viewport with shadow, items with highlight + check indicator, scroll buttons and separators, over the unstyled @tamagui/ui Select behavior. Adapts to a Sheet on native. This is the exact source the registry ships — copy it into your app and the examples above import from your copy.

    components/tamagui/Select.tsx
    // Styled Select = the unstyled @tamagui/ui Select behavior primitive + the // default v2-look skin, layered here in `tamagui`. Single skin definition; the // shadcn registry item is generated from this file. Default chevron/check icons // are dependency-free glyphs (see IconGlyph) so `tamagui` stays lean and native- // bundleable — no react-native-svg pulled into the core package. Consumers can // pass their own icon components as children of the icon parts. // // Sizing is preset-based: `size` is one of xs sm md lg xl (default md), owned // inline here like every other skin. import { type ComponentSize, createStyledContext, type GetProps, styled, withStaticProperties, } from '@tamagui/core' import { Select as SelectBehavior, SelectNativeComponentContext, type SelectProps as SelectBehaviorProps, type SelectScopedProps, } from '@tamagui/select' import { SizableText } from '@tamagui/text' const IconGlyph = styled(SizableText, { displayName: 'SelectIconGlyph', color: 'color', userSelect: 'none', }) const ChevronDown = ({ size = 16 }: { size?: number }) => ( <IconGlyph fontSize={size} lineHeight={1}> ▾ </IconGlyph> ) const ChevronUp = ({ size = 16 }: { size?: number }) => ( <IconGlyph fontSize={size} lineHeight={1}> ▴ </IconGlyph> ) const Check = ({ size = 14 }: { size?: number }) => ( <IconGlyph fontSize={size} lineHeight={1}> ✓ </IconGlyph> ) export type SelectSize = ComponentSize | boolean const SelectContext = createStyledContext<{ size?: SelectSize }>({ size: 'md' }) const selectFrameSize = { xs: { paddingInline: '2', paddingBlock: '1', borderRadius: 'sm', gap: '1' }, sm: { paddingInline: '3', paddingBlock: '1.5', borderRadius: 'md', gap: '1.5' }, md: { paddingInline: '4', paddingBlock: '2', borderRadius: 'md', gap: '2' }, lg: { paddingInline: '6', paddingBlock: '2', borderRadius: 'md', gap: '2' }, xl: { paddingInline: '8', paddingBlock: '2.5', borderRadius: 'lg', gap: '2.5' }, } as const const selectTextSize = { xs: { fontSize: 'xs', lineHeight: 'xs' }, sm: { fontSize: 'sm', lineHeight: 'sm' }, md: { fontSize: 'sm', lineHeight: 'sm' }, lg: { fontSize: 'base', lineHeight: 'base' }, xl: { fontSize: 'lg', lineHeight: 'lg' }, } as const const selectItemSize = { xs: { gap: '1', paddingHorizontal: '2', paddingVertical: '1' }, sm: { gap: '1.5', paddingHorizontal: '3', paddingVertical: '1.5' }, md: { gap: '2', paddingHorizontal: '4', paddingVertical: '2' }, lg: { gap: '2', paddingHorizontal: '6', paddingVertical: '2' }, xl: { gap: '2.5', paddingHorizontal: '8', paddingVertical: '2.5' }, } as const const selectNativeSize = { // a native <select> ignores line-height, so it gets the control height // (line height plus vertical padding) plus its 1px border on each side, // and room for the chevron xs: { paddingInline: '2', paddingBlock: '1', borderRadius: 'sm', gap: '1', height: 26, paddingRight: 28, }, sm: { paddingInline: '3', paddingBlock: '1.5', borderRadius: 'md', gap: '1.5', height: 34, paddingRight: 32, }, md: { paddingInline: '4', paddingBlock: '2', borderRadius: 'md', gap: '2', height: 38, paddingRight: 36, }, lg: { paddingInline: '6', paddingBlock: '2', borderRadius: 'md', gap: '2', height: 42, paddingRight: 44, }, xl: { paddingInline: '8', paddingBlock: '2.5', borderRadius: 'lg', gap: '2.5', height: 50, paddingRight: 52, }, } as const const SelectNative = styled(SizableText, { displayName: 'SelectNative', render: 'select', backgroundColor: 'background hover:background-hover', borderColor: 'border-color', borderWidth: 1, color: 'color', outlineWidth: 0, userSelect: 'none', variants: { size: { ...selectNativeSize, true: selectNativeSize.md, }, } as const, defaultVariants: { size: 'md' }, }) export const SelectTrigger = styled(SelectBehavior.Trigger, { context: SelectContext, displayName: 'SelectTrigger', width: '100%', maxWidth: '100%', overflow: 'hidden', flexWrap: 'nowrap', backgroundColor: 'background hover:background-hover press:background-press', borderColor: 'border-color hover:border-color-hover', borderWidth: 1, justifyContent: 'space-between', outlineColor: 'focus-visible:outline-color', outlineStyle: 'focus-visible:solid', outlineWidth: 'focus-visible:2px', variants: { size: { ...selectFrameSize, true: selectFrameSize.md, }, } as const, defaultVariants: { size: 'md' }, }) export const SelectValue = styled(SelectBehavior.Value, { context: SelectContext, displayName: 'SelectValue', color: 'color', ellipsis: true, variants: { size: { ...selectTextSize, true: selectTextSize.md, }, } as const, defaultVariants: { size: 'md' }, }) export const SelectIcon = styled(SelectBehavior.Icon, { context: SelectContext, displayName: 'SelectIcon', marginLeft: 'auto', children: <ChevronDown />, }) export const SelectGroup = styled(SelectBehavior.Group, { displayName: 'SelectGroup', width: '100%', }) export const SelectLabel = styled(SelectBehavior.Label, { context: SelectContext, displayName: 'SelectLabel', color: 'color-10', fontWeight: '600', paddingHorizontal: 10, paddingVertical: 6, variants: { size: { ...selectTextSize, true: selectTextSize.md, }, } as const, defaultVariants: { size: 'md' }, }) export const SelectItem = styled(SelectBehavior.Item, { context: SelectContext, displayName: 'SelectItem', width: '100%', maxWidth: '100%', overflow: 'hidden', flexWrap: 'nowrap', justifyContent: 'space-between', cursor: 'default', outlineOffset: -1, borderRadius: 6, backgroundColor: 'hover:background-hover press:background-press focus:background-focus', outlineColor: 'focus-visible:outline-color', outlineStyle: 'focus-visible:solid', outlineWidth: 'focus-visible:1px', variants: { size: { ...selectItemSize, true: selectItemSize.md, }, } as const, defaultVariants: { size: 'md' }, }) export const SelectItemText = styled(SelectBehavior.ItemText, { context: SelectContext, displayName: 'SelectItemText', color: 'color', userSelect: 'none', ellipsis: true, variants: { size: { ...selectTextSize, true: selectTextSize.md, }, } as const, defaultVariants: { size: 'md' }, }) export const SelectItemIndicator = styled(SelectBehavior.ItemIndicator, { displayName: 'SelectItemIndicator', alignItems: 'center', justifyContent: 'center', marginLeft: 'auto', children: <Check size={14} />, }) export const SelectIndicator = styled(SelectBehavior.Indicator, { displayName: 'SelectIndicator', backgroundColor: 'background-focus', borderRadius: 6, }) export const SelectViewport = styled(SelectBehavior.Viewport, { displayName: 'SelectViewport', backgroundColor: 'background', borderColor: 'border-color', borderRadius: 10, borderWidth: 1, maxHeight: 300, padding: 4, boxShadow: '0 12px 28px rgba(0, 0, 0, 0.2)', }) export const SelectScrollUpButton = styled(SelectBehavior.ScrollUpButton, { displayName: 'SelectScrollUpButton', alignItems: 'center', backgroundColor: 'background', height: 28, justifyContent: 'center', children: <ChevronUp size={16} />, }) export const SelectScrollDownButton = styled(SelectBehavior.ScrollDownButton, { displayName: 'SelectScrollDownButton', alignItems: 'center', backgroundColor: 'background', height: 28, justifyContent: 'center', children: <ChevronDown size={16} />, }) export const SelectSeparator = styled(SelectBehavior.Separator, { displayName: 'SelectSeparator', backgroundColor: 'border-color', height: 1, marginVertical: 4, }) export type SelectRootProps< Value extends string, Multiple extends boolean | undefined = false, > = Omit<SelectScopedProps<SelectBehaviorProps<Value, Multiple>>, 'size'> & { size?: SelectSize } export function SelectRoot< Value extends string = string, Multiple extends boolean | undefined = false, >({ size = true, ...props }: SelectRootProps<Value, Multiple>) { return ( <SelectNativeComponentContext.Provider value={SelectNative}> <SelectContext.Provider size={size}> <SelectBehavior.Root<Value, Multiple> size={size} {...props} /> </SelectContext.Provider> </SelectNativeComponentContext.Provider> ) } export const selectParts = { Adapt: SelectBehavior.Adapt, Content: SelectBehavior.Content, FocusScope: SelectBehavior.FocusScope, Group: SelectGroup, Icon: SelectIcon, Indicator: SelectIndicator, Item: SelectItem, ItemIndicator: SelectItemIndicator, ItemText: SelectItemText, Label: SelectLabel, ScrollDownButton: SelectScrollDownButton, ScrollUpButton: SelectScrollUpButton, Separator: SelectSeparator, Trigger: SelectTrigger, Value: SelectValue, Viewport: SelectViewport, } export const Select = withStaticProperties(SelectRoot, { Root: SelectRoot, ...selectParts, })

    Dependencies

    yarn add @tamagui/core @tamagui/select @tamagui/text

    Expects theme tokens: background, background-hover, background-press, background-focus, border-color, border-color-hover, outline-color, color, color-10. Native: requires a Portal/Adapt provider at the app root; on native the Select adapts to a Sheet, so the Sheet native peer requirements apply when adaptation is used

    Need raw behavior without any skin? tamagui/unstyled re-exports the @tamagui/ui primitives (advanced).