Select
The select component provides a streamlined way for users to pick a single option from a collapsible list of choices. It conserves screen space while offering clear visual feedback and maintaining accessibility standards.
import {Select} from "@qualcomm-ui/react/select"Overview
- Each select uses the
selectCollectionhelper to manage the list of options. This creates aListCollectioninstance, documented below.
Examples
Simple
The simple API provides a standalone component with built-in layout.
<Select
className="w-48"
collection={cityCollection}
controlProps={{"aria-label": "City"}}
placeholder="Select a city"
/>
Composite
Build with the composite API for granular control. This API requires you to provide each subcomponent, but gives you full control over the structure and layout.
<Select.Root
className="w-48"
collection={cityCollection}
placeholder="Select a city"
>
<Select.Label>City</Select.Label>
<Select.Control>
<Select.ValueText />
<Select.ClearTrigger />
<Select.Indicator />
<Select.ErrorIndicator />
</Select.Control>
<Select.HiddenSelect />
<Portal>
<Select.Positioner>
<Select.Content>
{cityCollection.items.map((item) => {
const label = cityCollection.stringifyItem(item)
const value = cityCollection.getItemValue(item)
return (
<Select.Item key={value} item={item}>
<Select.ItemText>{label}</Select.ItemText>
<Select.ItemIndicator />
</Select.Item>
)
})}
</Select.Content>
</Select.Positioner>
</Portal>
</Select.Root>
ARIA Label
The Select's label is automatically associated with the input element for accessibility. If you omit the label, you should provide an aria-label to the control to give your select an accessible name.
<Select
className="w-48"
collection={cityCollection}
controlProps={{"aria-label": "City"}}
placeholder="Select a city"
/>
Content Width
The positioning.sameWidth property controls whether the width of the select content matches the width of the trigger element. The default is true.
<Select
className="w-48"
collection={cityCollection}
controlProps={{"aria-label": "City"}}
placeholder="Select a city"
positioning={{sameWidth: false}}
/>
Trigger Icon
Use the icon prop to add an icon to the start of the trigger element.
<Select
className="w-48"
collection={cityCollection}
controlProps={{"aria-label": "City"}}
icon={MapPin}
placeholder="Select a city"
/>
Items as Objects
The items prop can be an array of objects. Use the itemLabel and itemValue properties on the selectCollection to specify the label and value of each item.
const cityCollection = selectCollection({
itemLabel: (item) => item.name,
items: [
{name: "San Diego", value: "SD"},
{name: "Nashville", value: "NV"},
{name: "Denver", value: "DV"},
{name: "Miami", value: "MI"},
{name: "Las Vegas", value: "LV"},
{name: "New York City", value: "NYC"},
{name: "San Francisco", value: "SF"},
],
itemValue: (item) => item.value,
})
Sizes
This component supports three size options to accommodate different layout densities and design requirements.
The available sizes are sm, md, and lg. The default size is md.
import type {ReactElement} from "react"
import {selectCollection} from "@qualcomm-ui/core/select"
import {Select} from "@qualcomm-ui/react/select"
const cityCollection = selectCollection({
items: ["San Diego", "Dallas", "Denver"],
})
export function SelectSizesDemo(): ReactElement {
return (
<div className="flex flex-col items-center gap-4">
<Select
className="w-40"
collection={cityCollection}
controlProps={{"aria-label": "City"}}
placeholder="sm"
positioning={{sameWidth: true}}
size="sm"
/>
<Select
className="w-48"
collection={cityCollection}
controlProps={{"aria-label": "City"}}
placeholder="md"
positioning={{sameWidth: true}}
size="md"
/>
<Select
className="w-56"
collection={cityCollection}
controlProps={{"aria-label": "City"}}
placeholder="lg"
positioning={{sameWidth: true}}
size="lg"
/>
</div>
)
}Content Height
Change the height of the item container by adjusting the style of the Content element.
<Select
className="w-48"
collection={cityCollection}
contentProps={{style: {maxHeight: 240}}}
controlProps={{"aria-label": "City"}}
placeholder="Select a city"
positioning={{sameWidth: true}}
/>
Multiple Selection
Use the multiple prop to allow multiple selections.
The selectionIndicator prop controls how selected items are displayed:
"checkmark"— Uses a checkmark after the selected options only (default)"checkbox"— Uses a checkbox before each option
<Select
className="w-72"
collection={cityCollection}
controlProps={{"aria-label": "City"}}
multiple
placeholder="Select cities"
selectionIndicator="checkbox"
/>
Controlled State
Set the initial value using the defaultValue prop, or use value and onValueChange to control the value manually. These props follow our controlled state pattern.
<Select
className="w-48"
collection={cityCollection}
controlProps={{"aria-label": "City"}}
onValueChange={setValue}
placeholder="Select a city"
value={value}
/>
States
The following shows how the Select component appears in each interactive state.
<Select
className="w-48"
collection={cityCollection}
defaultValue={["San Diego"]}
disabled
label="Disabled"
/>
<Select
className="w-48"
collection={cityCollection}
defaultValue={["San Diego"]}
label="Read only"
readOnly
/>
<Select
className="w-48"
collection={cityCollection}
defaultValue={["San Diego"]}
errorText="Invalid"
invalid
label="Invalid"
/>
Error Text and Indicator
Error messages are displayed using two props:
The error text and indicator will only render when invalid is true.
<Select
className="w-48"
collection={cityCollection}
controlProps={{"aria-label": "City"}}
errorText="You must select a city"
invalid={!value.length}
onValueChange={setValue}
placeholder="Select a city"
value={value}
/>
Within Dialog
To use the Select within a Dialog, you need to avoid portalling the Select.Positioner to the document's body. To do this using the simple API, set portalProps.disabled to true:
import type {ReactElement} from "react"
import {selectCollection} from "@qualcomm-ui/core/select"
import {Button} from "@qualcomm-ui/react/button"
import {Dialog} from "@qualcomm-ui/react/dialog"
import {Select} from "@qualcomm-ui/react/select"
const cityCollection = selectCollection({
items: [
"San Diego",
"Nashville",
"Denver",
"Miami",
"Las Vegas",
"New York City",
"San Francisco",
],
})
export function SelectWithinDialogDemo(): ReactElement {
return (
<Dialog.Root>
<Dialog.Trigger>
<Button emphasis="primary" variant="fill">
Open Dialog
</Button>
</Dialog.Trigger>
<Dialog.FloatingPortal>
<Dialog.Body>
<Dialog.Heading>Dialog Title</Dialog.Heading>
<Dialog.CloseButton />
<Select
className="w-48"
collection={cityCollection}
controlProps={{"aria-label": "City"}}
placeholder="Select a city"
portalProps={{disabled: true}}
/>
</Dialog.Body>
<Dialog.Footer>
<Dialog.CloseTrigger>
<Button emphasis="primary" size="sm" variant="fill">
Confirm
</Button>
</Dialog.CloseTrigger>
</Dialog.Footer>
</Dialog.FloatingPortal>
</Dialog.Root>
)
}Within Popover
Like with the Dialog, you need to avoid portalling the Select.Positioner to the document's body:
import type {ReactElement} from "react"
import {selectCollection} from "@qualcomm-ui/core/select"
import {Button} from "@qualcomm-ui/react/button"
import {Popover} from "@qualcomm-ui/react/popover"
import {Select} from "@qualcomm-ui/react/select"
const cityCollection = selectCollection({
items: [
"San Diego",
"Nashville",
"Denver",
"Miami",
"Las Vegas",
"New York City",
"San Francisco",
],
})
export function SelectWithinPopoverDemo(): ReactElement {
return (
<Popover
label="Select Example"
trigger={<Button emphasis="primary">Show Popover</Button>}
>
<Select
className="w-48"
collection={cityCollection}
label="City"
placeholder="Select a city"
portalProps={{disabled: true}}
/>
</Popover>
)
}Forms
Choose the form library that fits your needs—we've built examples with React Hook Form and Tanstack Form to get you started.
React Hook Form
Use React Hook Form to handle the input state and validation. ArkType works great for schema validation if you need it.
import type {ReactElement} from "react"
import {type} from "arktype"
import {Controller, type SubmitHandler, useForm} from "react-hook-form"
import {selectCollection} from "@qualcomm-ui/core/select"
import {Button} from "@qualcomm-ui/react/button"
import {Select} from "@qualcomm-ui/react/select"
import {createToaster, Toaster} from "@qualcomm-ui/react/toast"
const cityCollection = selectCollection({
items: [
"San Diego",
"Nashville",
"Denver",
"Miami",
"Las Vegas",
"New York City",
"San Francisco",
],
})
const valueSchema = type({
city: type("string[] > 0").configure({
message: "At least one city must be selected",
}),
})
type ValueSchema = typeof valueSchema.infer
const toaster = createToaster({
overlap: true,
placement: "bottom-end",
})
export function SelectHookFormDemo(): ReactElement {
const {
control,
formState: {isSubmitting},
handleSubmit,
setError,
} = useForm<ValueSchema>({
defaultValues: {
city: [],
},
})
const handleFormSubmit: SubmitHandler<ValueSchema> = (data) => {
const validation = valueSchema(data)
if (validation instanceof type.errors) {
validation.forEach((error) => {
const field = error.path?.[0] as keyof ValueSchema
if (field) {
setError(field, {
message: error.message,
})
}
})
return
}
toaster.create({
label: "Form submitted",
type: "success",
})
}
return (
<>
<Toaster toaster={toaster} />
<form
className="w-48"
noValidate
onSubmit={(event) => void handleSubmit(handleFormSubmit)(event)}
>
<Controller
control={control}
name="city"
render={({field: {onChange, ...fieldProps}, fieldState: {error}}) => (
<Select
className="w-full"
collection={cityCollection}
errorText={error?.message}
invalid={!!error}
label="City"
onValueChange={(valueStrings) => onChange(valueStrings)}
placeholder="Select a city"
required
{...fieldProps}
/>
)}
/>
<div className="mt-2 flex w-full justify-end">
<Button
disabled={isSubmitting}
emphasis="primary"
type="submit"
variant="fill"
>
Submit
</Button>
</div>
</form>
</>
)
}Tanstack Form
Tanstack Form handles validation with its built-in validators.
import type {ReactElement} from "react"
import {useForm} from "@tanstack/react-form"
import {selectCollection} from "@qualcomm-ui/core/select"
import {Button} from "@qualcomm-ui/react/button"
import {Select} from "@qualcomm-ui/react/select"
import {createToaster, Toaster} from "@qualcomm-ui/react/toast"
const cityCollection = selectCollection({
items: [
"San Diego",
"Nashville",
"Denver",
"Miami",
"Las Vegas",
"New York City",
"San Francisco",
],
})
const toaster = createToaster({
overlap: true,
placement: "bottom-end",
})
export function SelectTanstackFormDemo(): ReactElement {
const form = useForm({
defaultValues: {
city: [] as string[],
},
onSubmit: () => {
toaster.create({
label: "Form submitted",
type: "success",
})
},
})
return (
<>
<Toaster toaster={toaster} />
<form
className="w-48"
noValidate
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
<form.Field
name="city"
validators={{
onChange: ({value}) =>
value.length === 0
? "At least one city must be selected"
: undefined,
}}
>
{(field) => (
<Select
className="w-full"
collection={cityCollection}
errorText={field.state.meta.errors[0]}
invalid={field.state.meta.errors.length > 0}
label="City"
onValueChange={field.handleChange}
placeholder="Select a city"
required
value={field.state.value}
/>
)}
</form.Field>
<div className="mt-2 flex w-full justify-end">
<Button
disabled={form.state.isSubmitting}
emphasis="primary"
type="submit"
variant="fill"
>
Submit
</Button>
</div>
</form>
</>
)
}Tanstack form also supports ArkType.
Explorer
Component Anatomy
Hover to highlight, click to view API
API
<Select>
The simple select extends the Select.Root component with the following props:
stringstringbooleantrue, renders a clear button that resets the input value on click.
The button only appears when the input has a value.PortalPropsComposite API
This section describes the elements of the Select's composite API.
<Select.Root>
booleanstringbooleanstring[]
booleanThis is only applicable for single selection.
'ltr' | 'rtl'
booleanstring() =>
| Node
| ShadowRoot
| Document
stringstringPartial<
Omit<
{
clearTrigger: string
content: string
control: string
errorText: string
hiddenSelect: string
hint: string
label: string
positioner: string
root: string
},
| 'item'
| 'itemGroup'
| 'itemGroupLabel'
>
>
booleanbooleanbooleanbooleanbooleanstringname attribute of the underlying select.VoidFunction(
event: FocusOutsideEvent,
) => void
(
value: string,
details: {
highlightedIndex: number
highlightedItem: T
},
) => void
(
event: InteractOutsideEvent,
) => void
(
open: boolean,
) => void
- open:The next value of the open state.
(
event: PointerDownOutsideEvent,
) => void
(
value: string,
) => void
(
valueStrings: string[],
details: {items: Array<T>},
) => void
booleanstringbooleanboolean| ReactElement
| ((
props: object,
) => ReactElement)
boolean(details: {
immediate?: boolean
index: number
}) => void
| 'checkmark'
| 'checkbox'
| 'sm'
| 'md'
| 'lg'
booleanbooleanstring[]
<Select.Label>
<label> element by default.string| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-select__label'data-disableddata-invaliddata-readonlydata-select-part'label'<Select.Control>
<div> element
by default.| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-select__control'data-disableddata-invaliddata-placeholder-showndata-placement| 'bottom'
| 'bottom-end'
| 'bottom-start'
| 'left'
| 'left-end'
| 'left-start'
| 'right'
| 'right-end'
| 'right-start'
| 'top'
| 'top-end'
| 'top-start'
data-readonlydata-select-part'control'data-size| 'sm'
| 'md'
| 'lg'
data-state| 'open'
| 'closed'
tabIndexnumber<Select.Indicator>
<button> element by default.| ReactNode
| LucideIcon
| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-select__indicator'data-disableddata-invaliddata-readonlydata-select-part'indicator'data-size| 'sm'
| 'md'
| 'lg'
data-state| 'open'
| 'closed'
tabIndex-1
<Select.ValueText>
<span> element by default.| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-select__value-text'data-disableddata-focusdata-invaliddata-multipledata-select-part'value-text'data-size| 'sm'
| 'md'
| 'lg'
<Select.ClearTrigger>
<button> element by default.string| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-select__clear-trigger'data-invaliddata-select-part'clear-trigger'data-size| 'sm'
| 'md'
| 'lg'
hiddenboolean<Select.Positioner>
<div> element by
default.string| ReactElement
| ((
props: object,
) => ReactElement)
<Select.Content>
<div> element by default.| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-select__content'data-activedescendantstringdata-focus-visibledata-placement| 'bottom'
| 'bottom-end'
| 'bottom-start'
| 'left'
| 'left-end'
| 'left-start'
| 'right'
| 'right-end'
| 'right-start'
| 'top'
| 'top-end'
| 'top-start'
data-select-part'content'data-state| 'open'
| 'closed'
hiddenbooleantabIndex0<Select.Item>
<div> element by default.anyboolean| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-select__item'data-disableddata-highlighteddata-select-part'item'data-selection-indicator| 'checkmark'
| 'checkbox'
data-size| 'sm'
| 'md'
| 'lg'
data-state| 'checked'
| 'unchecked'
data-valuestring<Select.ItemCheckbox>
Checkbox-style indicator for select items. Use this instead of Select.ItemIndicator when selectionIndicator="checkbox" is set on the root. The checkbox is always visible and fills when the item is selected.
selectionIndicator="checkbox" on the
Select root.booleanboolean| ReactElement
| ((
props: object,
) => ReactElement)
| 'sm'
| 'md'
| 'lg'
<Select.ItemIndicator>
<span>
element by default.| ReactElement
| ((
props: object,
) => ReactElement)
data-select-part'item-indicator'data-state| 'checked'
| 'unchecked'
hiddenboolean<Select.ItemText>
| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-select__item-text'data-disableddata-highlighteddata-select-part'item-text'data-state| 'checked'
| 'unchecked'
<Select.HiddenSelect>
<select> element.className'qui-select__hidden-select'data-select-part'hidden-select'styletabIndex-1
<Select.Hint>
<div> element by default.| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-input__hint'data-disableddata-select-part'hint'hiddenboolean<Select.ErrorText>
<div> element by
default.| LucideIcon
| ReactNode
string| ReactElement
| ((
props: object,
) => ReactElement)
className'qui-input__error-text'data-select-part'error-text'hiddenboolean<Select.ErrorIndicator>
<div> element
by default.| LucideIcon
| ReactNode
| ReactElement
| ((
props: object,
) => ReactElement)
data-select-part'error-indicator'hiddenbooleanData Structures
ListCollection
The following describes the member variables and methods of the ListCollection class. The Select component uses an instance of this class as input:
const collection = selectCollection({
items: [
// ...
],
})
return <Select collection={collection} />Note that the ListCollection accepts a single generic type parameter, T, which is the type of each list item used in the collection. This can be a string or an object.
Constructor
(
item: T,
index: number,
) => string
| string[]
| 'desc'
| 'asc'
| ((
a: string,
b: string,
) => number)
(
item: T,
) => boolean
(
item: T,
) => string
| Iterable<T, any, any>
| Readonly<
Iterable<T, any, any>
>
(
item: T,
) => string
(
items: Array<T>,
) => ListCollection<T>
(
index: number,
) => T
(
a: string,
b: string,
) => -1 | 0 | 1
(items Array<T>,) => ListCollection<T>
(
fn: (
itemString: string,
index: number,
item: T,
) => boolean,
) => ListCollection<T>
(
value: string,
) => T
(
values: string[],
) => Array<T>
string(
T,
) => boolean
(
T,
) => string
(
value: string,
step: number,
clamp: boolean,
) => string
(
value: string,
step: number,
clamp: boolean,
) => string
(
from: string,
to: string,
) => string[]
(
items: Array<T>,
) => string[]
() => Array<
[string, Array<T>]
>
(
value: string,
) => boolean
(
T,
) => boolean
(
value: string,
) => number
(
index: number,
items: Array<T>,
) => ListCollection<T>
(
value: string,
items: Array<T>,
) => ListCollection<T>
(
value: string,
items: Array<T>,
) => ListCollection<T>
(
any,
) => boolean
Array<T>
string(
value: string,
toIndex: number,
) => ListCollection<T>
(
value: string,
values: string[],
) => ListCollection<T>
(
value: string,
values: string[],
) => ListCollection<T>
(
items: Array<T>,
) => ListCollection<T>
(
itemsOrValues: Array<
string | T
>,
) => ListCollection<T>
(
fromIndex: number,
toIndex: number,
) => ListCollection<T>
(
queryString: string,
{
currentValue: string
state: {
keysSoFar: string
timer: number
}
timeout?: number
},
) => string
(
items: Array<T>,
) => ListCollection<T>
number(
values: string[],
) => string[]
(
value: string,
) => string
(
T,
) => string
(
items: Array<T>,
separator: string,
) => string
(value: string[],separator string,) => string
() => {
first: string
last: string
size: number
}
() => string
(
value: string,
T,
) => ListCollection<T>
(
value: string,
T,
mode: 'append' | 'prepend',
) => ListCollection<T>
SelectPositioningOptions
numberstring[data-menu-part=arrow]).() =>
| 'clippingAncestors'
| Element
| Array<Element>
| {
height: number
width: number
x: number
y: number
}
boolean| boolean
| Array<
| 'bottom'
| 'bottom-end'
| 'bottom-start'
| 'left'
| 'left-end'
| 'left-start'
| 'right'
| 'right-end'
| 'right-start'
| 'top'
| 'top-end'
| 'top-start'
>
(
element:
| HTMLElement
| VirtualElement,
) => {
height?: number
width?: number
x?: number
y?: number
}
numberboolean| boolean
| {
ancestorResize?: boolean
ancestorScroll?: boolean
animationFrame?: boolean
elementResize?: boolean
layoutShift?: boolean
}
{
crossAxis?: number
mainAxis?: number
}
(
data: ComputePositionReturn,
) => void
(data: {
placed: boolean
}) => void
numberboolean| 'bottom'
| 'bottom-end'
| 'bottom-start'
| 'left'
| 'left-end'
| 'left-start'
| 'right'
| 'right-end'
| 'right-start'
| 'top'
| 'top-end'
| 'top-start'
booleannumberboolean| 'absolute'
| 'fixed'
(data: {
updatePosition: () => Promise<void>
}) => void | Promise<void>
<div>element by default.