Skip to main content

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..." />
}
Live Editor
<Selector
  color="primary"
  placeholder="Select a fruit..."
  options={[
    { value: 'apple', label: 'Apple' },
    { value: 'banana', label: 'Banana' },
    { value: 'cherry', label: 'Cherry' },
  ]}
/>
Result
Loading...

Variants

VariantDescription
borderedFull border, transparent background (default)
fadedFull border, tinted background
flatTinted background, no border
underlinedBottom border only, no radius
Live Editor
<Selector
  variant="bordered"
  placeholder="Bordered"
  options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]}
/>
Result
Loading...

Colors

  • default
  • primary
  • secondary
  • success
  • warning
  • error

Variant affects colors. Have a look to Storybook to see how the component is rendering depending on the variant.

Live Editor
<Selector
  color="primary"
  placeholder="Primary"
  options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]}
/>
Result
Loading...

Sizes

  • sm
  • md
  • lg
Live Editor
<Selector
  size="sm"
  placeholder="Small"
  options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]}
/>
Result
Loading...

Radius

  • none
  • sm
  • md
  • lg
  • full
Live Editor
<Selector
  radius="sm"
  placeholder="Rounded"
  options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]}
/>
Result
Loading...

sm, md and lg map to --easyui-radius-* CSS variables, see the theming guide.

The underlined variant always uses rounded-none regardless of the radius prop.

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.

Live Editor
<Selector
  startContent={<Apple size={16} />}
  placeholder="Select a fruit..."
  options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]}
/>
Result
Loading...

Arrow

A ChevronDown icon is shown by default at the end of the trigger, and rotates when the listbox opens.

  • isArrowHidden removes it entirely
  • arrowPlacement moves it to the start or end (default) of the trigger
  • arrow replaces the default icon with any node
Live Editor
<Selector
  arrowPlacement="start"
  placeholder="Arrow on the left"
  options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]}
/>
Result
Loading...

States

Loading

Set isLoading to show a spinner and disable the selector.

Live Editor
<Selector
  isLoading
  placeholder="Loading..."
  options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]}
/>
Result
Loading...

Disabled

Live Editor
<Selector
  isDisabled
  placeholder="Disabled"
  options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]}
/>
Result
Loading...

Full width

Live Editor
<Selector
  isFullWidth
  placeholder="Full width"
  options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]}
/>
Result
Loading...

Label & description

Live Editor
<Selector
  label="Fruit"
  description="Pick your favorite fruit."
  placeholder="Select a fruit..."
  options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]}
/>
Result
Loading...

Use descriptionPlacement to render the description below the label instead of below the selector:

Live Editor
<Selector
  label="Fruit"
  description="Pick your favorite fruit."
  descriptionPlacement="label"
  placeholder="Select a fruit..."
  options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]}
/>
Result
Loading...

Required

Add isRequired to mark the field as required — an asterisk appears next to the label.

Live Editor
<Selector
  label="Fruit"
  isRequired
  placeholder="Select a fruit..."
  options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]}
/>
Result
Loading...

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.

Live Editor
<Selector
  label="Fruit"
  error="Please select a fruit."
  placeholder="Select a fruit..."
  options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]}
/>
Result
Loading...

No options

When the options list is empty, the listbox shows a customizable "No results found".

Live Editor
<Selector placeholder="Select a fruit..." options={[]} noResultsMessage="No fruit available" />
Result
Loading...

Selection mode

selectionMode switches between selecting one value ('single', the default) and several ('multiple'). It also switches the type of value, defaultValue and onValueChange:

selectionModevalue / defaultValueonValueChange
'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} />
Live Editor
<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' },
  ]}
/>
Result
Loading...

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.

Live Editor
<Selector
  selectionMode="multiple"
  selectionIndicator="none"
  placeholder="Select fruits..."
  defaultValue={['apple']}
  options={[
    { value: 'apple', label: 'Apple' },
    { value: 'banana', label: 'Banana' },
    { value: 'cherry', label: 'Cherry' },
  ]}
/>
Result
Loading...

Options

Each entry in options accepts:

FieldTypeDescription
valuestringUnique value, passed to onValueChange
labelstringText shown in the trigger and the listbox
descriptionstringHelper text shown below the option label
isDisabledbooleanPrevents selecting this option
startContentReactNodeIcon (or any node) shown before the label
endContentReactNodeIcon (or any node) shown after the label
Live Editor
<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' },
  ]}
/>
Result
Loading...

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.

Live Editor
<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' },
  ]}
/>
Result
Loading...

Slots

Customize individual parts via classNames:

SlotDescription
baseRoot <div> wrapping the whole field
labelThe <label> element
triggerThe trigger <button>
valueWrapper around the selected value/placeholder
chipsWrapper around the chips (multiple mode)
chipEach selected value chip
chipLabelThe label inside a chip
chipRemoveButtonThe remove control inside a chip
startContentWrapper around startContent
endContentWrapper around endContent
arrowWrapper around the arrow icon
spinnerThe loading spinner
listboxThe options <ul> container
optionEach <li> option
descriptionThe helper text element
errorThe error message element
Live Editor
<Selector
  classNames={{ trigger: 'shadow-md', listbox: 'shadow-lg' }}
  placeholder="Custom slots"
  options={[{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]}
/>
Result
Loading...

These slots can also be styled globally for every Selector in your app, see the global configuration guide.

Props

PropTypeDefaultDescription
optionsSelectorOption[]-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
valuestring (single) | string[] (multiple)-Controlled selected value
defaultValuestring (single) | string[] (multiple)-Initial value for uncontrolled usage
onValueChange(value: string) => void | (values: string[]) => void-Called when the selection changes
placeholderstring-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
isDisabledbooleanfalseDisables the selector
isLoadingbooleanfalseShows a spinner and disables the selector
isRequiredbooleanfalseMarks the field as required
isRequiredMessagestring-Message shown when a required field has no selection
isFullWidthbooleanfalseMakes the selector take the full available width
labelstring-Label rendered above the trigger
descriptionstring-Helper text
descriptionPlacement'label' | 'element''element'Render description below the label or below the trigger
errorstring-Externally controlled error, takes precedence over local validation
validationsArray<(option: SelectorOption) => string | null>-Per-option validators; invalid options are disabled
noResultsMessagestring'No results found'Message shown when the options list is empty
startContentReactNode-Content rendered before the value
endContentReactNode-Content rendered after the value
startContentPlacement'inside' | 'outside''inside'Where startContent is rendered
endContentPlacement'inside' | 'outside''inside'Where endContent is rendered
arrowReactNode-Custom node replacing the default chevron icon
arrowPlacement'start' | 'end''end'Where the arrow is rendered
isArrowHiddenbooleanfalseHides the arrow
classNamestring-Class applied to the root element
classNamesPartial<Record<SelectorSlots, string>>-Per-slot class overrides

Selector forwards ref to the underlying trigger <button> element.

Storybook

Open in Storybook → and see more combinations.