Selector
A custom-styled dropdown select for choosing one or several values among a list of options.
Usage
import { Selector } from '@easy-ui-react/easy-ui-react'
const fruitOptions = [
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
{ value: 'cherry', label: 'Cherry' },
]
export function Example() {
const [value, setValue] = useState<string>()
return <Selector options={fruitOptions} value={value} onValueChange={setValue} placeholder="Select a fruit..." />
}
<Selector color="primary" placeholder="Select a fruit..." options={[ { value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }, { value: 'cherry', label: 'Cherry' }, ]} />
Variants
| Variant | Description |
|---|---|
bordered | Full border, transparent background (default) |
faded | Full border, tinted background |
flat | Tinted background, no border |
underlined | Bottom border only, no radius |
<Selector variant="bordered" placeholder="Bordered" options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]} />
Colors
defaultprimarysecondarysuccesswarningerror
Variant affects colors. Have a look to Storybook to see how the component is rendering depending on the variant.
<Selector color="primary" placeholder="Primary" options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]} />
Sizes
smmdlg
<Selector size="sm" placeholder="Small" options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]} />
Radius
nonesmmdlgfull
<Selector radius="sm" placeholder="Rounded" options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]} />
sm, md and lg map to --easyui-radius-* CSS variables, see the theming guide.
The
underlinedvariant always usesrounded-noneregardless of theradiusprop.
Content
Add an icon (or any node) before/after the value with startContent / endContent.
By default, they sit inside the trigger box. Set startContentPlacement / endContentPlacement to outside to render them outside it.
<Selector startContent={<Apple size={16} />} placeholder="Select a fruit..." options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]} />
Arrow
A ChevronDown icon is shown by default at the end of the trigger, and rotates when the listbox opens.
isArrowHiddenremoves it entirelyarrowPlacementmoves it to thestartorend(default) of the triggerarrowreplaces the default icon with any node
<Selector arrowPlacement="start" placeholder="Arrow on the left" options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]} />
States
Loading
Set isLoading to show a spinner and disable the selector.
<Selector isLoading placeholder="Loading..." options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]} />
Disabled
<Selector isDisabled placeholder="Disabled" options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]} />
Full width
<Selector isFullWidth placeholder="Full width" options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]} />
Label & description
<Selector label="Fruit" description="Pick your favorite fruit." placeholder="Select a fruit..." options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]} />
Use descriptionPlacement to render the description below the label instead of below the selector:
<Selector label="Fruit" description="Pick your favorite fruit." descriptionPlacement="label" placeholder="Select a fruit..." options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]} />
Required
Add isRequired to mark the field as required — an asterisk appears next to the label.
<Selector label="Fruit" isRequired placeholder="Select a fruit..." options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]} />
Error
Pass an error string to show a message below the selector. This is the externally controlled error channel: use it for errors the field cannot produce on its own (a server-side response, an async check, a cross-field rule). It always takes precedence over the field's own validation (isRequired, per-option validations), so a non-empty error is always the message shown.
<Selector label="Fruit" error="Please select a fruit." placeholder="Select a fruit..." options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]} />
No options
When the options list is empty, the listbox shows a customizable "No results found".
<Selector placeholder="Select a fruit..." options={[]} noResultsMessage="No fruit available" />
Selection mode
selectionMode switches between selecting one value ('single', the default) and several ('multiple'). It also switches the type of value, defaultValue and onValueChange:
selectionMode | value / defaultValue | onValueChange |
|---|---|---|
'single' | string | (value: string) => void |
'multiple' | string[] | (values: string[]) => void |
In multiple mode, the trigger shows one removable chip per selected value, clicking an already selected option deselects it, and the listbox stays open between selections. Backspace on the focused trigger removes the last selected value.
const [values, setValues] = useState<string[]>([])
<Selector selectionMode="multiple" options={fruitOptions} value={values} onValueChange={setValues} />
<Selector selectionMode="multiple" color="primary" placeholder="Select fruits..." defaultValue={['apple', 'banana']} options={[ { value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }, { value: 'cherry', label: 'Cherry' }, { value: 'date', label: 'Date' }, ]} />
Chips are removed with a <span role="button"> rather than a real <button>, because the trigger is itself a <button> and cannot contain interactive elements. Keyboard users deselect a value from the listbox, or with Backspace on the trigger.
Selection indicator
In the listbox, selected options are marked with a check icon at the very end of the option, after its endContent (selectionIndicator="check", the default), in both selection modes. Set selectionIndicator="none" to keep only the bolder label as the selected state.
The space for the icon is reserved on the other options so their content stays aligned; since the icon sits on the trailing edge, labels and startContent are never shifted.
<Selector selectionMode="multiple" selectionIndicator="none" placeholder="Select fruits..." defaultValue={['apple']} options={[ { value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }, { value: 'cherry', label: 'Cherry' }, ]} />
Options
Each entry in options accepts:
| Field | Type | Description |
|---|---|---|
value | string | Unique value, passed to onValueChange |
label | string | Text shown in the trigger and the listbox |
description | string | Helper text shown below the option label |
isDisabled | boolean | Prevents selecting this option |
startContent | ReactNode | Icon (or any node) shown before the label |
endContent | ReactNode | Icon (or any node) shown after the label |
<Selector placeholder="Select a fruit..." options={[ { value: 'apple', label: 'Apple', description: 'Crisp and sweet', startContent: <Apple size={16} /> }, { value: 'banana', label: 'Banana', description: 'Soft and creamy' }, ]} />
Validation
Unlike text fields, validations runs against each option, not the selected value. It is an array of (option: SelectorOption) => string | null functions: an option for which a validator returns a message is disabled, and the message is shown as that option's description. If the currently selected value becomes invalid, it is automatically deselected.
The only value-level check is isRequired, which verifies that an option is selected.
<Selector placeholder="Select a fruit..." validations={[(option) => (option.value === 'cherry' ? 'Out of season' : null)]} options={[ { value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }, { value: 'cherry', label: 'Cherry' }, ]} />
Slots
Customize individual parts via classNames:
| Slot | Description |
|---|---|
base | Root <div> wrapping the whole field |
label | The <label> element |
trigger | The trigger <button> |
value | Wrapper around the selected value/placeholder |
chips | Wrapper around the chips (multiple mode) |
chip | Each selected value chip |
chipLabel | The label inside a chip |
chipRemoveButton | The remove control inside a chip |
startContent | Wrapper around startContent |
endContent | Wrapper around endContent |
arrow | Wrapper around the arrow icon |
spinner | The loading spinner |
listbox | The options <ul> container |
option | Each <li> option |
description | The helper text element |
error | The error message element |
<Selector classNames={{ trigger: 'shadow-md', listbox: 'shadow-lg' }} placeholder="Custom slots" options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]} />
These slots can also be styled globally for every Selector in your app, see the global configuration guide.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
options | SelectorOption[] | - | The list of selectable options |
selectionMode | 'single' | 'multiple' | 'single' | Whether one or several options can be selected |
selectionIndicator | 'check' | 'none' | 'check' | Check icon at the end of selected options in the listbox |
value | string (single) | string[] (multiple) | - | Controlled selected value |
defaultValue | string (single) | string[] (multiple) | - | Initial value for uncontrolled usage |
onValueChange | (value: string) => void | (values: string[]) => void | - | Called when the selection changes |
placeholder | string | - | Text shown when no value is selected |
variant | 'bordered' | 'faded' | 'flat' | 'underlined' | 'bordered' | Visual style |
color | 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'default' | Focus color |
size | 'sm' | 'md' | 'lg' | 'md' | Selector size |
radius | 'none' | 'sm' | 'md' | 'lg' | 'full' | 'md' | Corner radius |
isDisabled | boolean | false | Disables the selector |
isLoading | boolean | false | Shows a spinner and disables the selector |
isRequired | boolean | false | Marks the field as required |
isRequiredMessage | string | - | Message shown when a required field has no selection |
isFullWidth | boolean | false | Makes the selector take the full available width |
label | string | - | Label rendered above the trigger |
description | string | - | Helper text |
descriptionPlacement | 'label' | 'element' | 'element' | Render description below the label or below the trigger |
error | string | - | Externally controlled error, takes precedence over local validation |
validations | Array<(option: SelectorOption) => string | null> | - | Per-option validators; invalid options are disabled |
noResultsMessage | string | 'No results found' | Message shown when the options list is empty |
startContent | ReactNode | - | Content rendered before the value |
endContent | ReactNode | - | Content rendered after the value |
startContentPlacement | 'inside' | 'outside' | 'inside' | Where startContent is rendered |
endContentPlacement | 'inside' | 'outside' | 'inside' | Where endContent is rendered |
arrow | ReactNode | - | Custom node replacing the default chevron icon |
arrowPlacement | 'start' | 'end' | 'end' | Where the arrow is rendered |
isArrowHidden | boolean | false | Hides the arrow |
className | string | - | Class applied to the root element |
classNames | Partial<Record<SelectorSlots, string>> | - | Per-slot class overrides |
Selector forwards ref to the underlying trigger <button> element.
Storybook
Open in Storybook → and see more combinations.